From 869802f9e20ef3b0d0226ad14bad0632dc343a3e Mon Sep 17 00:00:00 2001 From: Marco Boeck Date: Wed, 5 Jun 2019 11:35:32 +0200 Subject: [PATCH] Release RapidMiner 9.3.0 --- build.gradle | 6 +- gradle.properties | 2 +- .../repository/wsimport/EntryResponse.java | 27 + src/main/java/com/rapidminer/Process.java | 87 +- src/main/java/com/rapidminer/RapidMiner.java | 5 + .../connection/ConnectionHandler.java | 57 + .../connection/ConnectionHandlerRegistry.java | 63 + .../ConnectionHandlerRegistryListener.java | 29 + .../connection/ConnectionInformation.java | 82 + .../ConnectionInformationBuilder.java | 181 ++ ...onnectionInformationContainerIOObject.java | 117 ++ .../ConnectionInformationFileUtils.java | 363 ++++ .../connection/ConnectionInformationImpl.java | 131 ++ .../ConnectionInformationSerializer.java | 556 +++++ .../connection/ConnectionStatistics.java | 59 + .../ConnectionStatisticsBuilder.java | 64 + .../connection/ConnectionStatisticsImpl.java | 144 ++ .../CreateI18NKeysForConnectionHandler.java | 504 +++++ .../connection/DefaultValueProviderGUI.java | 123 ++ .../connection/adapter/ConnectionAdapter.java | 54 + .../adapter/ConnectionAdapterException.java | 57 + .../adapter/ConnectionAdapterHandler.java | 769 +++++++ .../configuration/ConfigurationParameter.java | 65 + .../ConfigurationParameterGroup.java | 45 + .../ConfigurationParameterGroupImpl.java | 98 + .../ConfigurationParameterImpl.java | 111 + .../ConnectionConfiguration.java | 149 ++ .../ConnectionConfigurationBuilder.java | 233 +++ .../ConnectionConfigurationImpl.java | 284 +++ .../configuration/PlaceholderParameter.java | 39 + .../PlaceholderParameterImpl.java | 85 + .../connection/gui/AbstractConnectionGUI.java | 467 +++++ .../gui/ConnectionCreationDialog.java | 588 ++++++ .../connection/gui/ConnectionEditDialog.java | 293 +++ .../connection/gui/ConnectionGUI.java | 91 + .../connection/gui/ConnectionGUIProvider.java | 50 + .../connection/gui/ConnectionGUIRegistry.java | 84 + .../gui/ConnectionInformationRenderer.java | 68 + .../gui/ConnectionListCellRenderer.java | 46 + .../connection/gui/DefaultConnectionGUI.java | 119 ++ .../gui/DefaultConnectionGUIProvider.java | 39 + .../gui/InjectParametersDialog.java | 451 ++++ .../gui/UnknownConnectionTypeGUI.java | 53 + .../gui/UnknownConnectionTypeGUIProvider.java | 40 + .../gui/ValueProviderGUIProvider.java | 110 + .../gui/ValueProviderGUIRegistry.java | 102 + .../gui/actions/CancelEditingAction.java | 67 + .../gui/actions/InjectParametersAction.java | 82 + .../gui/actions/OpenEditConnectionAction.java | 60 + .../gui/actions/SaveConnectionAction.java | 193 ++ .../gui/actions/TestConnectionAction.java | 193 ++ .../gui/components/ConnectionInfoPanel.java | 334 +++ .../ConnectionParameterCheckBox.java | 59 + .../components/ConnectionParameterLabel.java | 76 + .../ConnectionParameterTextField.java | 132 ++ .../components/ConnectionSourcesPanel.java | 222 ++ .../components/ConnectionTagEditPanel.java | 220 ++ .../gui/components/DeprecationWarning.java | 127 ++ .../InjectableComponentWrapper.java | 74 + .../InjectedParameterPlaceholderLabel.java | 150 ++ .../gui/components/InjectionPanel.java | 75 + .../gui/components/TestConnectionPanel.java | 215 ++ .../gui/dto/ConnectionInformationHolder.java | 189 ++ .../listener/TextChangedDocumentListener.java | 72 + .../connection/gui/model/ConnectionModel.java | 413 ++++ .../gui/model/ConnectionModelConverter.java | 218 ++ .../model/ConnectionParameterGroupModel.java | 152 ++ .../gui/model/ConnectionParameterModel.java | 228 ++ .../gui/model/InjectParametersModel.java | 102 + .../gui/model/PlaceholderParameterModel.java | 97 + .../gui/model/ValueProviderModel.java | 139 ++ .../model/ValueProviderModelConverter.java | 71 + .../model/ValueProviderParameterModel.java | 161 ++ .../legacy/ConversionException.java | 44 + .../connection/legacy/ConversionService.java | 55 + .../connection/util/ConnectionI18N.java | 468 +++++ .../util/ConnectionInformationSelector.java | 468 +++++ .../util/ConnectionSelectionProvider.java | 38 + .../connection/util/GenericHandler.java | 71 + .../util/GenericHandlerRegistry.java | 191 ++ .../GenericRegistrationEventListener.java | 41 + .../connection/util/ProgressAdapter.java | 57 + .../connection/util/RegistrationEvent.java | 54 + .../connection/util/TestExecutionContext.java | 66 + .../util/TestExecutionContextImpl.java | 94 + .../connection/util/TestResult.java | 170 ++ .../connection/util/ValidationResult.java | 117 ++ .../valueprovider/ValueProvider.java | 54 + .../valueprovider/ValueProviderImpl.java | 145 ++ .../valueprovider/ValueProviderParameter.java | 71 + .../ValueProviderParameterImpl.java | 220 ++ .../handler/BaseValueProviderHandler.java | 143 ++ .../handler/ChainingValueProviderHandler.java | 207 ++ .../handler/MacroValueProviderGUI.java | 38 + .../handler/MacroValueProviderHandler.java | 159 ++ .../handler/ValueProviderHandler.java | 93 + .../handler/ValueProviderHandlerRegistry.java | 363 ++++ .../ValueProviderHandlerRegistryListener.java | 29 + .../handler/ValueProviderUtils.java | 61 + .../core/license/ProductLinkRegistry.java | 21 +- .../table/internal/DoubleAutoSparseChunk.java | 4 +- .../internal/IntegerAutoSparseChunk.java | 4 +- .../java/com/rapidminer/gui/MainFrame.java | 48 +- .../rapidminer/gui/MetaDataUpdateQueue.java | 5 +- .../com/rapidminer/gui/OperatorDocLoader.java | 3 +- .../gui/OperatorDocToHtmlConverter.java | 14 +- .../com/rapidminer/gui/StaticButtonModel.java | 158 ++ ...seAllResultsExceptCurrentResultAction.java | 51 + .../actions/CopyStringToClipboardAction.java | 60 + .../gui/actions/CreateConnectionAction.java | 43 + .../rapidminer/gui/actions/OpenAction.java | 55 +- .../gui/actions/UpgradeLicenseAction.java | 7 +- .../gui/actions/export/ComponentPrinter.java | 21 +- .../com/rapidminer/gui/dialog/EULADialog.java | 2 +- .../gui/dialog/OperatorInfoScreen.java | 121 +- .../dnd/ReceivingOperatorTransferHandler.java | 24 + .../view/ProcessRendererView.java | 38 +- .../gui/graphs/SingleDefaultGraphMouse.java | 17 +- .../ExtendedPickingGraphMousePlugin.java | 18 + .../rapidminer/gui/look/RapidLookAndFeel.java | 3 +- .../gui/look/RapidLookComboBoxEditor.java | 14 +- .../gui/look/fc/MultipleLinesLabel.java | 2 +- .../gui/look/ui/MultiStepProgressBar.java | 17 +- .../rapidminer/gui/look/ui/TabbedPaneUI.java | 31 +- .../jfreechart/JFreeChartPlotEngine.java | 5 +- .../gui/operatormenu/ReplaceOperatorMenu.java | 131 +- .../actions/CutCopyPasteDeleteAction.java | 16 +- .../com/rapidminer/gui/popup/PopupPanel.java | 8 +- .../gui/processeditor/MacroEditor.java | 7 +- .../results/DockableResultDisplay.java | 15 +- .../processeditor/results/ResultDisplay.java | 24 +- .../gui/processeditor/results/ResultTab.java | 5 +- .../results/SingleResultOverview.java | 12 +- .../OperatorGlobalSearchGUIProvider.java | 6 +- .../AdditionalPermissionsListener.java | 18 + .../properties/AttributeOrderingDialog.java | 82 +- .../properties/AttributesPropertyDialog.java | 66 +- .../properties/ExpressionPropertyDialog.java | 106 +- .../gui/properties/MacroSelectionDialog.java | 102 +- .../gui/properties/PropertyPanel.java | 3 + .../gui/properties/RegexpPropertyDialog.java | 1850 +++++++++-------- .../gui/properties/SettingsDialog.java | 6 +- .../gui/properties/SettingsItems.java | 13 +- .../celleditors/value/AttributeComboBox.java | 7 +- .../value/AttributeValueCellEditor.java | 11 +- .../ConnectionLocationValueCellEditor.java | 53 + .../value/ProcessLocationValueCellEditor.java | 141 +- .../value/RegexpValueCellEditor.java | 53 +- .../value/RemoteFileValueCellEditor.java | 48 +- .../RepositoryLocationValueCellEditor.java | 22 +- ...itoryLocationWithExtraValueCellEditor.java | 177 ++ .../implementations/CellTypeDateImpl.java | 12 +- .../CellTypeTextFieldDefaultImpl.java | 29 +- .../AbstractDataTablePlotterRenderer.java | 2 + .../gui/renderer/RendererService.java | 28 +- .../renderer/data/ExampleSetPlotRenderer.java | 2 + .../math/NumericalMatrixPlotRenderer.java | 2 + .../math/RainflowMatrixPlotRenderer.java | 2 + .../models/KernelModelPlotRenderer.java | 2 + ...PolynomialRegressionModelPlotRenderer.java | 3 +- .../weights/AttributeWeightsPlotRenderer.java | 2 + .../gui/search/GlobalSearchController.java | 5 +- .../gui/search/GlobalSearchDialog.java | 4 + .../gui/search/GlobalSearchGUIUtilities.java | 6 +- .../BlacklistedOperatorProcessEditor.java | 6 +- .../rapidminer/gui/tools/FilterTextField.java | 16 +- .../gui/tools/FilterableListModel.java | 50 +- .../com/rapidminer/gui/tools/IconSize.java | 11 +- .../gui/tools/MultiSwingWorker.java | 64 + .../rapidminer/gui/tools/ProgressThread.java | 135 +- .../gui/tools/ProgressThreadDialog.java | 17 +- .../gui/tools/ProgressThreadDisplay.java | 15 +- .../rapidminer/gui/tools/ResourceAction.java | 7 +- .../com/rapidminer/gui/tools/SwingTools.java | 110 +- .../gui/tools/TextFieldWithAction.java | 11 +- .../rapidminer/gui/tools/VersionNumber.java | 4 +- .../SuccessiveExecutionTimer.java | 7 +- .../gui/tools/bubble/WindowChoreographer.java | 2 +- .../gui/tools/color/ColorSlider.java | 8 +- .../tools/components/AbstractLinkButton.java | 17 +- .../gui/tools/components/FeedbackForm.java | 5 +- .../gui/tools/components/FixedWidthLabel.java | 8 + .../gui/tools/components/ToolTipWindow.java | 5 +- .../dnd/ExtendedJListTransferHandler.java | 8 +- .../MetaDataStatisticsController.java | 242 ++- .../metadata/MetaDataStatisticsViewer.java | 121 +- .../CopyAllMetaDataToClipboardAction.java | 132 -- .../com/rapidminer/operator/Annotations.java | 100 +- .../rapidminer/operator/DummyOperator.java | 17 +- .../com/rapidminer/operator/Operator.java | 266 ++- .../rapidminer/operator/OperatorChain.java | 19 +- .../operator/SimpleProcessSetupError.java | 2 +- .../UndefinedParameterSetupError.java | 55 + .../UserSpecificationDataGenerator.java | 3 +- .../operator/io/AbstractReader.java | 24 +- .../operator/io/ExcelExampleSetWriter.java | 774 ++++--- .../operator/io/RepositorySource.java | 71 +- .../operator/io/RepositoryStorer.java | 13 + .../meta/ParameterIteratingOperatorChain.java | 11 +- .../operator/nio/StoreDataWizardStep.java | 2 +- .../nio/file/FileInputPortHandler.java | 2 +- .../AbstractPerformanceEvaluator.java | 6 +- .../operator/performance/AreaUnderCurve.java | 31 +- .../BinaryClassificationPerformance.java | 43 +- ...nalClassificationPerformanceEvaluator.java | 174 +- .../operator/ports/DummyPortPairExtender.java | 45 +- .../com/rapidminer/operator/ports/Port.java | 6 +- .../operator/ports/PortPairExtender.java | 200 +- .../operator/ports/impl/AbstractPort.java | 38 +- .../ConnectionInformationMetaData.java | 188 ++ .../ports/metadata/MDTransformer.java | 5 +- .../operator/ports/metadata/MetaData.java | 13 +- .../ports/metadata/MetaDataFactory.java | 24 +- .../quickfix/ParameterSettingQuickFix.java | 16 +- .../preprocessing/PreprocessingOperator.java | 5 +- .../RemoveUnusedNominalValuesOperator.java | 15 +- .../filter/AttributeValueMapper.java | 39 +- .../filter/AttributeValueReplace.java | 32 +- .../filter/ChangeAttributeNamesReplace.java | 36 +- .../filter/NominalToBinominal.java | 64 +- .../preprocessing/filter/Real2Integer.java | 40 +- .../preprocessing/join/ExampleSetMerge.java | 2 +- .../visualization/ProcessLogOperator.java | 294 ++- .../ROCBasedComparisonOperator.java | 10 +- .../dependencies/NumericalMatrix.java | 46 + .../rapidminer/parameter/ParameterType.java | 27 +- .../parameter/ParameterTypeAttribute.java | 59 +- .../ParameterTypeConnectionLocation.java | 51 + .../parameter/ParameterTypeEnumeration.java | 98 +- .../parameter/ParameterTypeExpression.java | 67 +- .../parameter/ParameterTypeInnerOperator.java | 1 + .../parameter/ParameterTypeList.java | 64 +- .../ParameterTypeOperatorParameterTupel.java | 113 + .../parameter/ParameterTypeRegexp.java | 41 + .../parameter/ParameterTypeTupel.java | 17 +- .../parameter/ParameterTypeValue.java | 26 +- .../com/rapidminer/parameter/Parameters.java | 51 +- .../SimpleListBasedParameterHandler.java | 8 +- .../com/rapidminer/repository/BlobEntry.java | 1 + .../repository/ConnectionEntry.java | 71 + .../java/com/rapidminer/repository/Entry.java | 2 + .../rapidminer/repository/EntryCreator.java | 73 + .../com/rapidminer/repository/Folder.java | 104 +- .../rapidminer/repository/IOObjectEntry.java | 2 + .../PersistentContentMapperStore.java | 4 +- .../rapidminer/repository/ProcessEntry.java | 1 + .../com/rapidminer/repository/Repository.java | 15 + ...onConditionAdditionallyNotConnections.java | 109 + ...RepositoryActionConditionImplStandard.java | 20 +- ...tionConditionImplStandardNoRepository.java | 16 +- ...tionConditionRepositoryAndConnections.java | 76 + ...ryConnectionsFolderDuplicateException.java | 44 + ...ryConnectionsFolderImmutableException.java | 44 + ...itoryConnectionsNotSupportedException.java | 44 + .../RepositoryEntryNotFoundException.java | 7 + .../RepositoryEntryWrongTypeException.java | 7 + .../repository/RepositoryFilter.java | 65 + .../repository/RepositoryLocation.java | 6 +- .../repository/RepositoryManager.java | 164 +- ...positoryNotConnectionsFolderException.java | 45 + ...toreOtherInConnectionsFolderException.java | 45 + .../repository/RepositoryTools.java | 80 + .../repository/gui/LocalRepositoryPanel.java | 2 +- .../repository/gui/RepositoryBrowser.java | 34 +- .../repository/gui/RepositoryTree.java | 251 ++- .../gui/RepositoryTreeCellRenderer.java | 57 +- .../repository/gui/RepositoryTreeModel.java | 27 +- .../gui/actions/AbstractRepositoryAction.java | 11 + .../gui/actions/CreateConnectionAction.java | 66 + .../gui/actions/EditConnectionAction.java | 131 ++ .../gui/actions/OpenEntryAction.java | 5 +- .../actions/RenameRepositoryEntryAction.java | 10 +- .../ShowProcessInRepositoryAction.java | 3 + .../repository/gui/actions/SortByAction.java | 38 +- .../RepositoryGlobalSearchGUIProvider.java | 24 +- .../internal/remote/RESTRepository.java | 18 + .../remote/RemoteConnectionEntry.java | 31 + .../internal/remote/RemoteContentManager.java | 108 +- .../remote/RemoteCreateVaultInformation.java | 89 + .../internal/remote/RemoteRepository.java | 70 +- .../remote/RemoteRepositoryFactory.java | 67 +- .../internal/remote/RemoteVaultEntry.java | 122 ++ .../repository/local/LocalRepository.java | 52 +- .../repository/local/SimpleBlobEntry.java | 49 +- .../local/SimpleConnectionEntry.java | 171 ++ .../repository/local/SimpleDataEntry.java | 65 +- .../repository/local/SimpleEntry.java | 44 +- .../repository/local/SimpleFolder.java | 232 ++- .../repository/local/SimpleIOObjectEntry.java | 225 +- .../repository/local/SimpleProcessEntry.java | 61 +- .../resource/ResourceBlobEntry.java | 22 +- .../resource/ResourceConnectionEntry.java | 80 + .../repository/resource/ResourceFolder.java | 66 +- .../resource/ResourceIOObjectEntry.java | 78 +- .../resource/ResourceProcessEntry.java | 2 +- .../resource/ZipResourceConnectionEntry.java | 46 + .../resource/ZipResourceFolder.java | 63 +- .../resource/ZipResourceIOObjectEntry.java | 6 - .../search/RepositoryGlobalSearch.java | 3 +- .../search/RepositoryGlobalSearchItem.java | 45 + .../search/RepositoryGlobalSearchManager.java | 66 +- .../search/GlobalSearchCategory.java | 51 +- .../search/GlobalSearchIndexer.java | 38 +- .../search/GlobalSearchManager.java | 11 + .../search/GlobalSearchRegistry.java | 11 +- .../search/GlobalSearchUtilities.java | 2 +- .../internal/RecursiveWrapper.java | 2 +- .../rapidminer/studio/internal/Resources.java | 12 +- .../gui/internal/DataImportWizardUtils.java | 2 +- .../steps/AbstractToRepositoryStep.java | 8 +- .../com/rapidminer/template/Template.java | 6 +- .../java/com/rapidminer/test_utils/Util.java | 2 +- .../rapidminer/tools/ClassLoaderSwapper.java | 77 + .../tools/DefaultMailSessionFactory.java | 102 +- .../tools/ExtensibleResourceBundle.java | 8 +- .../rapidminer/tools/FileSystemService.java | 8 +- .../java/com/rapidminer/tools/FontTools.java | 2 +- .../tools/FunctionWithThrowable.java | 103 + .../tools/IteratorEnumerationAdapter.java | 127 ++ .../com/rapidminer/tools/ListenerTools.java | 37 +- .../com/rapidminer/tools/OperatorService.java | 5 +- .../rapidminer/tools/ParameterService.java | 25 +- .../com/rapidminer/tools/ProcessTools.java | 190 +- .../com/rapidminer/tools/RMUrlHandler.java | 3 + src/main/java/com/rapidminer/tools/Tools.java | 60 +- .../com/rapidminer/tools/ValidationUtil.java | 586 ++++++ .../rapidminer/tools/XMLParserException.java | 17 +- .../tools/config/AbstractConfigurator.java | 1 + .../rapidminer/tools/config/Configurable.java | 54 +- .../config/ConfigurableConnectionHandler.java | 33 + .../gui/ConfigurableAdminPasswordDialog.java | 3 +- .../tools/config/gui/ConfigurableDialog.java | 113 +- .../tools/config/jwt/JwtReader.java | 95 +- .../com/rapidminer/tools/container/Pair.java | 27 +- .../expression/ExpressionParserBuilder.java | 5 + .../internal/SimpleExpressionEvaluator.java | 42 +- ...itraryStringInputStringOutputFunction.java | 34 +- .../internal/function/AbstractFunction.java | 12 +- .../function/eval/AbstractEvaluation.java | 355 ++++ .../function/eval/AttributeEvaluation.java | 80 + .../eval/AttributeEvaluationException.java | 48 + .../internal/function/eval/Evaluation.java | 319 +-- .../internal/function/eval/TypeConstants.java | 4 +- .../tools/math/ROCDataGenerator.java | 80 +- .../parameter/admin/ParameterEnforcer.java | 6 +- .../com/rapidminer/tools/plugin/Plugin.java | 33 + .../usagestats/ActionStatisticsCollector.java | 74 +- .../tools/usagestats/RuleService.java | 10 +- .../com/rapidminer/tutorial/Tutorial.java | 6 +- .../com/rapidminer/resources/EULA_EN.txt | 126 +- .../rapidminer/resources/groups.properties | 1 + .../resources/i18n/Errors.properties | 70 +- .../rapidminer/resources/i18n/GUI.properties | 406 +++- .../resources/i18n/LogMessages.properties | 58 +- .../resources/i18n/Settings.properties | 4 + .../i18n/UserErrorMessages.properties | 44 +- .../resources/icons/16/@2x/academy_link.png | Bin 0 -> 1133 bytes .../resources/icons/16/@2x/academy_page.png | Bin 0 -> 821 bytes .../resources/icons/16/@2x/academy_video.png | Bin 0 -> 981 bytes .../resources/icons/16/@2x/gearwheel_left.png | Bin 0 -> 1776 bytes .../icons/16/@2x/gearwheel_right.png | Bin 0 -> 1784 bytes .../resources/icons/16/@2x/injection2.png | Bin 0 -> 1298 bytes .../resources/icons/16/@2x/question_blue.png | Bin 0 -> 1255 bytes .../resources/icons/16/@2x/sources.png | Bin 0 -> 1164 bytes .../icons/16/@2x/test_connection.png | Bin 0 -> 1024 bytes .../resources/icons/16/academy_link.png | Bin 0 -> 585 bytes .../resources/icons/16/academy_page.png | Bin 0 -> 475 bytes .../resources/icons/16/academy_video.png | Bin 0 -> 527 bytes .../resources/icons/16/gearwheel_left.png | Bin 0 -> 813 bytes .../resources/icons/16/gearwheel_right.png | Bin 0 -> 796 bytes .../resources/icons/16/injection2.png | Bin 0 -> 616 bytes .../resources/icons/16/question_blue.png | Bin 0 -> 610 bytes .../rapidminer/resources/icons/16/sources.png | Bin 0 -> 530 bytes .../resources/icons/16/test_connection.png | Bin 0 -> 526 bytes .../resources/icons/24/@2x/gearwheel_left.png | Bin 0 -> 2901 bytes .../icons/24/@2x/gearwheel_right.png | Bin 0 -> 2929 bytes .../resources/icons/24/@2x/injection2.png | Bin 0 -> 2141 bytes .../resources/icons/24/@2x/question_blue.png | Bin 0 -> 1895 bytes .../resources/icons/24/@2x/sources.png | Bin 0 -> 1662 bytes .../icons/24/@2x/test_connection.png | Bin 0 -> 1586 bytes .../resources/icons/24/gearwheel_left.png | Bin 0 -> 1321 bytes .../resources/icons/24/gearwheel_right.png | Bin 0 -> 1321 bytes .../resources/icons/24/injection2.png | Bin 0 -> 983 bytes .../resources/icons/24/question_blue.png | Bin 0 -> 921 bytes .../rapidminer/resources/icons/24/sources.png | Bin 0 -> 758 bytes .../resources/icons/24/test_connection.png | Bin 0 -> 833 bytes .../icons/32/@2x/@2x/question_blue.png | Bin 0 -> 5551 bytes .../resources/icons/32/@2x/gearwheel_left.png | Bin 0 -> 3975 bytes .../icons/32/@2x/gearwheel_right.png | Bin 0 -> 4076 bytes .../resources/icons/32/@2x/injection2.png | Bin 0 -> 2713 bytes .../resources/icons/32/@2x/plug.png | Bin 0 -> 1133 bytes .../resources/icons/32/@2x/question_blue.png | Bin 0 -> 2592 bytes .../resources/icons/32/@2x/sources.png | Bin 0 -> 2309 bytes .../icons/32/@2x/test_connection.png | Bin 0 -> 2029 bytes .../resources/icons/32/gearwheel_left.png | Bin 0 -> 1776 bytes .../resources/icons/32/gearwheel_right.png | Bin 0 -> 1784 bytes .../resources/icons/32/injection2.png | Bin 0 -> 1298 bytes .../rapidminer/resources/icons/32/plug.png | Bin 0 -> 723 bytes .../resources/icons/32/question_blue.png | Bin 0 -> 1255 bytes .../rapidminer/resources/icons/32/sources.png | Bin 0 -> 1164 bytes .../resources/icons/32/test_connection.png | Bin 0 -> 1024 bytes .../resources/icons/48/@2x/gearwheel_left.png | Bin 0 -> 6153 bytes .../icons/48/@2x/gearwheel_right.png | Bin 0 -> 6190 bytes .../resources/icons/48/@2x/injection2.png | Bin 0 -> 4461 bytes .../resources/icons/48/@2x/question_blue.png | Bin 0 -> 3948 bytes .../resources/icons/48/@2x/sources.png | Bin 0 -> 3678 bytes .../icons/48/@2x/test_connection.png | Bin 0 -> 3054 bytes .../resources/icons/48/gearwheel_left.png | Bin 0 -> 2901 bytes .../resources/icons/48/gearwheel_right.png | Bin 0 -> 2929 bytes .../resources/icons/48/injection2.png | Bin 0 -> 2141 bytes .../resources/icons/48/question_blue.png | Bin 0 -> 1895 bytes .../rapidminer/resources/icons/48/sources.png | Bin 0 -> 2309 bytes .../resources/icons/48/test_connection.png | Bin 0 -> 1586 bytes .../resources/icons/64/@2x/gearwheel_left.png | Bin 0 -> 8172 bytes .../icons/64/@2x/gearwheel_right.png | Bin 0 -> 8330 bytes .../resources/icons/64/@2x/question_blue.png | Bin 0 -> 5551 bytes .../resources/icons/64/gearwheel_left.png | Bin 0 -> 3975 bytes .../resources/icons/64/gearwheel_right.png | Bin 0 -> 4076 bytes .../resources/icons/64/question_blue.png | Bin 0 -> 2592 bytes .../resources/icons/96/@2x/andrews_curves.png | Bin 16397 -> 0 bytes .../resources/icons/96/@2x/area.png | Bin 5587 -> 0 bytes .../resources/icons/96/@2x/areaspline.png | Bin 5737 -> 0 bytes .../resources/icons/96/@2x/areastep.png | Bin 3031 -> 0 bytes .../rapidminer/resources/icons/96/@2x/bar.png | Bin 3235 -> 0 bytes .../resources/icons/96/@2x/bellcurve.png | Bin 5619 -> 0 bytes .../resources/icons/96/@2x/boxplot.png | Bin 4514 -> 0 bytes .../resources/icons/96/@2x/column.png | Bin 3035 -> 0 bytes .../resources/icons/96/@2x/deviation.png | Bin 11197 -> 0 bytes .../resources/icons/96/@2x/funnel.png | Bin 5085 -> 0 bytes .../resources/icons/96/@2x/heatmap.png | Bin 3132 -> 0 bytes .../resources/icons/96/@2x/histogram.png | Bin 3082 -> 0 bytes .../resources/icons/96/@2x/line.png | Bin 7227 -> 0 bytes .../resources/icons/96/@2x/packedbubble.png | Bin 9000 -> 0 bytes .../icons/96/@2x/parallel_coordinates.png | Bin 14243 -> 0 bytes .../resources/icons/96/@2x/pareto.png | Bin 7934 -> 0 bytes .../rapidminer/resources/icons/96/@2x/pie.png | Bin 8620 -> 0 bytes .../resources/icons/96/@2x/pyramid.png | Bin 6845 -> 0 bytes .../resources/icons/96/@2x/rangearea.png | Bin 7376 -> 0 bytes .../icons/96/@2x/rangeareaspline.png | Bin 8500 -> 0 bytes .../resources/icons/96/@2x/rangeareastep.png | Bin 3283 -> 0 bytes .../resources/icons/96/@2x/rangecolumn.png | Bin 2999 -> 0 bytes .../resources/icons/96/@2x/rangeerrorbar.png | Bin 3520 -> 0 bytes .../resources/icons/96/@2x/sankey.png | Bin 13205 -> 0 bytes .../resources/icons/96/@2x/scatter.png | Bin 6678 -> 0 bytes .../resources/icons/96/@2x/scatter3d.png | Bin 9727 -> 0 bytes .../resources/icons/96/@2x/scatter_matrix.png | Bin 7836 -> 0 bytes .../resources/icons/96/@2x/spline.png | Bin 7156 -> 0 bytes .../resources/icons/96/@2x/stepline.png | Bin 3600 -> 0 bytes .../resources/icons/96/@2x/streamgraph.png | Bin 9507 -> 0 bytes .../resources/icons/96/@2x/treemap.png | Bin 3259 -> 0 bytes .../resources/icons/96/@2x/vector.png | Bin 4296 -> 0 bytes .../resources/icons/96/@2x/wordcloud.png | Bin 7020 -> 0 bytes .../com/rapidminer/resources/ioobjects.xml | 31 +- .../com/rapidminer/resources/settings.xml | 3 + .../ConnectionInformationBuilderTest.java | 146 ++ .../ConnectionInformationFileUtilsTest.java | 313 +++ .../adapter/ConnectionAdapterHandlerTest.java | 133 ++ .../ConfigurationParameterImplTest.java | 135 ++ .../ConnectionConfigurationBuilderTest.java | 312 +++ .../configuration/ConnectionResources.java | 41 + .../ParameterInjectionDialogExample.java | 92 + .../model/ConnectionModelConverterTest.java | 187 ++ .../ConnectionParameterGroupModelTest.java | 57 + .../model/ConnectionParameterModelTest.java | 70 + .../gui/model/MyConnectionParameterModel.java | 41 + .../legacy/ConversionServiceTest.java | 107 + .../util/TestExecutionContextImplTest.java | 96 + .../connection/util/ValidationResultTest.java | 83 + .../ValueProviderParameterImplTest.java | 149 ++ .../ValueProviderParameterTest.java | 260 +++ .../valueprovider/ValueProviderUtilsTest.java | 61 + .../handler/BaseValueProviderHandlerTest.java | 132 ++ .../ChainingValueProviderHandlerTest.java | 181 ++ .../MacroValueProviderHandlerTest.java | 115 + .../ValueProviderHandlerRegistryTest.java | 389 ++++ .../table/internal/AutoColumnTest.java | 66 + .../gui/properties/MessageTruncationTest.java | 118 ++ .../ConnectionInformationMetaDataTest.java | 119 ++ .../ParameterTypeRenameAndReplaceTest.java | 302 +++ .../local/LocalRepositoryFolderTest.java | 326 +++ .../resource/ConcurrentRepositoryTest.java | 131 +- .../resource/ResourceFolderTest.java | 177 ++ .../search/GlobalSearchCategoryTest.java | 19 +- .../search/GlobalSearchRegistryTest.java | 4 + .../rapidminer/tools/ListenerToolsTest.java | 194 ++ .../rapidminer/tools/ValidationUtilTest.java | 155 ++ .../tools/container/StackingMapTest.java | 10 +- .../AntlrParserAttributeEvalTest.java | 334 +++ .../rapidminer/tools/net/UrlFollowerTest.java | 8 +- .../tools/plugin/ManagedExtensionTest.java | 10 +- .../rapidminer/tools/plugin/PluginTest.java | 10 +- .../com/rapidminer/connection/README | 3 + .../com/rapidminer/connection/empty.jar | Bin 0 -> 22 bytes .../com/rapidminer/connection/empty.txt | 0 .../rapidminer/connection/encoding-test.txt | 1 + .../rapidminer/repository/resource/README.txt | 2 + .../resources/resourcerepositorytest/CONTENTS | 1 + .../Connections/CONTENTS | 1 + .../Connections/one.conninfo | Bin 0 -> 354693 bytes 499 files changed, 30909 insertions(+), 4489 deletions(-) create mode 100644 src/main/java/com/rapidminer/connection/ConnectionHandler.java create mode 100644 src/main/java/com/rapidminer/connection/ConnectionHandlerRegistry.java create mode 100644 src/main/java/com/rapidminer/connection/ConnectionHandlerRegistryListener.java create mode 100644 src/main/java/com/rapidminer/connection/ConnectionInformation.java create mode 100644 src/main/java/com/rapidminer/connection/ConnectionInformationBuilder.java create mode 100644 src/main/java/com/rapidminer/connection/ConnectionInformationContainerIOObject.java create mode 100644 src/main/java/com/rapidminer/connection/ConnectionInformationFileUtils.java create mode 100644 src/main/java/com/rapidminer/connection/ConnectionInformationImpl.java create mode 100644 src/main/java/com/rapidminer/connection/ConnectionInformationSerializer.java create mode 100644 src/main/java/com/rapidminer/connection/ConnectionStatistics.java create mode 100644 src/main/java/com/rapidminer/connection/ConnectionStatisticsBuilder.java create mode 100644 src/main/java/com/rapidminer/connection/ConnectionStatisticsImpl.java create mode 100644 src/main/java/com/rapidminer/connection/CreateI18NKeysForConnectionHandler.java create mode 100644 src/main/java/com/rapidminer/connection/DefaultValueProviderGUI.java create mode 100644 src/main/java/com/rapidminer/connection/adapter/ConnectionAdapter.java create mode 100644 src/main/java/com/rapidminer/connection/adapter/ConnectionAdapterException.java create mode 100644 src/main/java/com/rapidminer/connection/adapter/ConnectionAdapterHandler.java create mode 100644 src/main/java/com/rapidminer/connection/configuration/ConfigurationParameter.java create mode 100644 src/main/java/com/rapidminer/connection/configuration/ConfigurationParameterGroup.java create mode 100644 src/main/java/com/rapidminer/connection/configuration/ConfigurationParameterGroupImpl.java create mode 100644 src/main/java/com/rapidminer/connection/configuration/ConfigurationParameterImpl.java create mode 100644 src/main/java/com/rapidminer/connection/configuration/ConnectionConfiguration.java create mode 100644 src/main/java/com/rapidminer/connection/configuration/ConnectionConfigurationBuilder.java create mode 100644 src/main/java/com/rapidminer/connection/configuration/ConnectionConfigurationImpl.java create mode 100644 src/main/java/com/rapidminer/connection/configuration/PlaceholderParameter.java create mode 100644 src/main/java/com/rapidminer/connection/configuration/PlaceholderParameterImpl.java create mode 100644 src/main/java/com/rapidminer/connection/gui/AbstractConnectionGUI.java create mode 100644 src/main/java/com/rapidminer/connection/gui/ConnectionCreationDialog.java create mode 100644 src/main/java/com/rapidminer/connection/gui/ConnectionEditDialog.java create mode 100644 src/main/java/com/rapidminer/connection/gui/ConnectionGUI.java create mode 100644 src/main/java/com/rapidminer/connection/gui/ConnectionGUIProvider.java create mode 100644 src/main/java/com/rapidminer/connection/gui/ConnectionGUIRegistry.java create mode 100644 src/main/java/com/rapidminer/connection/gui/ConnectionInformationRenderer.java create mode 100644 src/main/java/com/rapidminer/connection/gui/ConnectionListCellRenderer.java create mode 100644 src/main/java/com/rapidminer/connection/gui/DefaultConnectionGUI.java create mode 100644 src/main/java/com/rapidminer/connection/gui/DefaultConnectionGUIProvider.java create mode 100644 src/main/java/com/rapidminer/connection/gui/InjectParametersDialog.java create mode 100644 src/main/java/com/rapidminer/connection/gui/UnknownConnectionTypeGUI.java create mode 100644 src/main/java/com/rapidminer/connection/gui/UnknownConnectionTypeGUIProvider.java create mode 100644 src/main/java/com/rapidminer/connection/gui/ValueProviderGUIProvider.java create mode 100644 src/main/java/com/rapidminer/connection/gui/ValueProviderGUIRegistry.java create mode 100644 src/main/java/com/rapidminer/connection/gui/actions/CancelEditingAction.java create mode 100644 src/main/java/com/rapidminer/connection/gui/actions/InjectParametersAction.java create mode 100644 src/main/java/com/rapidminer/connection/gui/actions/OpenEditConnectionAction.java create mode 100644 src/main/java/com/rapidminer/connection/gui/actions/SaveConnectionAction.java create mode 100644 src/main/java/com/rapidminer/connection/gui/actions/TestConnectionAction.java create mode 100644 src/main/java/com/rapidminer/connection/gui/components/ConnectionInfoPanel.java create mode 100644 src/main/java/com/rapidminer/connection/gui/components/ConnectionParameterCheckBox.java create mode 100644 src/main/java/com/rapidminer/connection/gui/components/ConnectionParameterLabel.java create mode 100644 src/main/java/com/rapidminer/connection/gui/components/ConnectionParameterTextField.java create mode 100644 src/main/java/com/rapidminer/connection/gui/components/ConnectionSourcesPanel.java create mode 100644 src/main/java/com/rapidminer/connection/gui/components/ConnectionTagEditPanel.java create mode 100644 src/main/java/com/rapidminer/connection/gui/components/DeprecationWarning.java create mode 100644 src/main/java/com/rapidminer/connection/gui/components/InjectableComponentWrapper.java create mode 100644 src/main/java/com/rapidminer/connection/gui/components/InjectedParameterPlaceholderLabel.java create mode 100644 src/main/java/com/rapidminer/connection/gui/components/InjectionPanel.java create mode 100644 src/main/java/com/rapidminer/connection/gui/components/TestConnectionPanel.java create mode 100644 src/main/java/com/rapidminer/connection/gui/dto/ConnectionInformationHolder.java create mode 100644 src/main/java/com/rapidminer/connection/gui/listener/TextChangedDocumentListener.java create mode 100644 src/main/java/com/rapidminer/connection/gui/model/ConnectionModel.java create mode 100644 src/main/java/com/rapidminer/connection/gui/model/ConnectionModelConverter.java create mode 100644 src/main/java/com/rapidminer/connection/gui/model/ConnectionParameterGroupModel.java create mode 100644 src/main/java/com/rapidminer/connection/gui/model/ConnectionParameterModel.java create mode 100644 src/main/java/com/rapidminer/connection/gui/model/InjectParametersModel.java create mode 100644 src/main/java/com/rapidminer/connection/gui/model/PlaceholderParameterModel.java create mode 100644 src/main/java/com/rapidminer/connection/gui/model/ValueProviderModel.java create mode 100644 src/main/java/com/rapidminer/connection/gui/model/ValueProviderModelConverter.java create mode 100644 src/main/java/com/rapidminer/connection/gui/model/ValueProviderParameterModel.java create mode 100644 src/main/java/com/rapidminer/connection/legacy/ConversionException.java create mode 100644 src/main/java/com/rapidminer/connection/legacy/ConversionService.java create mode 100644 src/main/java/com/rapidminer/connection/util/ConnectionI18N.java create mode 100644 src/main/java/com/rapidminer/connection/util/ConnectionInformationSelector.java create mode 100644 src/main/java/com/rapidminer/connection/util/ConnectionSelectionProvider.java create mode 100644 src/main/java/com/rapidminer/connection/util/GenericHandler.java create mode 100644 src/main/java/com/rapidminer/connection/util/GenericHandlerRegistry.java create mode 100644 src/main/java/com/rapidminer/connection/util/GenericRegistrationEventListener.java create mode 100644 src/main/java/com/rapidminer/connection/util/ProgressAdapter.java create mode 100644 src/main/java/com/rapidminer/connection/util/RegistrationEvent.java create mode 100644 src/main/java/com/rapidminer/connection/util/TestExecutionContext.java create mode 100644 src/main/java/com/rapidminer/connection/util/TestExecutionContextImpl.java create mode 100644 src/main/java/com/rapidminer/connection/util/TestResult.java create mode 100644 src/main/java/com/rapidminer/connection/util/ValidationResult.java create mode 100644 src/main/java/com/rapidminer/connection/valueprovider/ValueProvider.java create mode 100644 src/main/java/com/rapidminer/connection/valueprovider/ValueProviderImpl.java create mode 100644 src/main/java/com/rapidminer/connection/valueprovider/ValueProviderParameter.java create mode 100644 src/main/java/com/rapidminer/connection/valueprovider/ValueProviderParameterImpl.java create mode 100644 src/main/java/com/rapidminer/connection/valueprovider/handler/BaseValueProviderHandler.java create mode 100644 src/main/java/com/rapidminer/connection/valueprovider/handler/ChainingValueProviderHandler.java create mode 100644 src/main/java/com/rapidminer/connection/valueprovider/handler/MacroValueProviderGUI.java create mode 100644 src/main/java/com/rapidminer/connection/valueprovider/handler/MacroValueProviderHandler.java create mode 100644 src/main/java/com/rapidminer/connection/valueprovider/handler/ValueProviderHandler.java create mode 100644 src/main/java/com/rapidminer/connection/valueprovider/handler/ValueProviderHandlerRegistry.java create mode 100644 src/main/java/com/rapidminer/connection/valueprovider/handler/ValueProviderHandlerRegistryListener.java create mode 100644 src/main/java/com/rapidminer/connection/valueprovider/handler/ValueProviderUtils.java create mode 100644 src/main/java/com/rapidminer/gui/StaticButtonModel.java create mode 100644 src/main/java/com/rapidminer/gui/actions/CloseAllResultsExceptCurrentResultAction.java create mode 100644 src/main/java/com/rapidminer/gui/actions/CopyStringToClipboardAction.java create mode 100644 src/main/java/com/rapidminer/gui/actions/CreateConnectionAction.java create mode 100644 src/main/java/com/rapidminer/gui/properties/celleditors/value/ConnectionLocationValueCellEditor.java create mode 100644 src/main/java/com/rapidminer/gui/properties/celleditors/value/RepositoryLocationWithExtraValueCellEditor.java create mode 100644 src/main/java/com/rapidminer/gui/tools/MultiSwingWorker.java delete mode 100644 src/main/java/com/rapidminer/gui/viewer/metadata/actions/CopyAllMetaDataToClipboardAction.java create mode 100644 src/main/java/com/rapidminer/operator/UndefinedParameterSetupError.java create mode 100644 src/main/java/com/rapidminer/operator/ports/metadata/ConnectionInformationMetaData.java create mode 100644 src/main/java/com/rapidminer/parameter/ParameterTypeConnectionLocation.java create mode 100644 src/main/java/com/rapidminer/parameter/ParameterTypeOperatorParameterTupel.java create mode 100644 src/main/java/com/rapidminer/repository/ConnectionEntry.java create mode 100644 src/main/java/com/rapidminer/repository/EntryCreator.java create mode 100644 src/main/java/com/rapidminer/repository/RepositoryActionConditionAdditionallyNotConnections.java create mode 100644 src/main/java/com/rapidminer/repository/RepositoryActionConditionRepositoryAndConnections.java create mode 100644 src/main/java/com/rapidminer/repository/RepositoryConnectionsFolderDuplicateException.java create mode 100644 src/main/java/com/rapidminer/repository/RepositoryConnectionsFolderImmutableException.java create mode 100644 src/main/java/com/rapidminer/repository/RepositoryConnectionsNotSupportedException.java create mode 100644 src/main/java/com/rapidminer/repository/RepositoryFilter.java create mode 100644 src/main/java/com/rapidminer/repository/RepositoryNotConnectionsFolderException.java create mode 100644 src/main/java/com/rapidminer/repository/RepositoryStoreOtherInConnectionsFolderException.java create mode 100644 src/main/java/com/rapidminer/repository/gui/actions/CreateConnectionAction.java create mode 100644 src/main/java/com/rapidminer/repository/gui/actions/EditConnectionAction.java create mode 100644 src/main/java/com/rapidminer/repository/internal/remote/RemoteConnectionEntry.java create mode 100644 src/main/java/com/rapidminer/repository/internal/remote/RemoteCreateVaultInformation.java create mode 100644 src/main/java/com/rapidminer/repository/internal/remote/RemoteVaultEntry.java create mode 100644 src/main/java/com/rapidminer/repository/local/SimpleConnectionEntry.java create mode 100644 src/main/java/com/rapidminer/repository/resource/ResourceConnectionEntry.java create mode 100644 src/main/java/com/rapidminer/repository/resource/ZipResourceConnectionEntry.java create mode 100644 src/main/java/com/rapidminer/tools/ClassLoaderSwapper.java create mode 100644 src/main/java/com/rapidminer/tools/FunctionWithThrowable.java create mode 100644 src/main/java/com/rapidminer/tools/IteratorEnumerationAdapter.java create mode 100644 src/main/java/com/rapidminer/tools/ValidationUtil.java create mode 100644 src/main/java/com/rapidminer/tools/config/ConfigurableConnectionHandler.java create mode 100644 src/main/java/com/rapidminer/tools/expression/internal/function/eval/AbstractEvaluation.java create mode 100644 src/main/java/com/rapidminer/tools/expression/internal/function/eval/AttributeEvaluation.java create mode 100644 src/main/java/com/rapidminer/tools/expression/internal/function/eval/AttributeEvaluationException.java create mode 100644 src/main/resources/com/rapidminer/resources/icons/16/@2x/academy_link.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/16/@2x/academy_page.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/16/@2x/academy_video.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/16/@2x/gearwheel_left.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/16/@2x/gearwheel_right.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/16/@2x/injection2.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/16/@2x/question_blue.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/16/@2x/sources.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/16/@2x/test_connection.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/16/academy_link.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/16/academy_page.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/16/academy_video.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/16/gearwheel_left.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/16/gearwheel_right.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/16/injection2.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/16/question_blue.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/16/sources.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/16/test_connection.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/24/@2x/gearwheel_left.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/24/@2x/gearwheel_right.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/24/@2x/injection2.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/24/@2x/question_blue.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/24/@2x/sources.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/24/@2x/test_connection.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/24/gearwheel_left.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/24/gearwheel_right.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/24/injection2.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/24/question_blue.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/24/sources.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/24/test_connection.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/32/@2x/@2x/question_blue.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/32/@2x/gearwheel_left.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/32/@2x/gearwheel_right.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/32/@2x/injection2.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/32/@2x/plug.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/32/@2x/question_blue.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/32/@2x/sources.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/32/@2x/test_connection.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/32/gearwheel_left.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/32/gearwheel_right.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/32/injection2.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/32/plug.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/32/question_blue.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/32/sources.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/32/test_connection.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/48/@2x/gearwheel_left.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/48/@2x/gearwheel_right.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/48/@2x/injection2.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/48/@2x/question_blue.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/48/@2x/sources.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/48/@2x/test_connection.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/48/gearwheel_left.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/48/gearwheel_right.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/48/injection2.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/48/question_blue.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/48/sources.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/48/test_connection.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/64/@2x/gearwheel_left.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/64/@2x/gearwheel_right.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/64/@2x/question_blue.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/64/gearwheel_left.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/64/gearwheel_right.png create mode 100644 src/main/resources/com/rapidminer/resources/icons/64/question_blue.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/andrews_curves.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/area.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/areaspline.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/areastep.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/bar.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/bellcurve.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/boxplot.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/column.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/deviation.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/funnel.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/heatmap.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/histogram.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/line.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/packedbubble.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/parallel_coordinates.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/pareto.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/pie.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/pyramid.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/rangearea.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/rangeareaspline.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/rangeareastep.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/rangecolumn.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/rangeerrorbar.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/sankey.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/scatter.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/scatter3d.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/scatter_matrix.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/spline.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/stepline.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/streamgraph.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/treemap.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/vector.png delete mode 100644 src/main/resources/com/rapidminer/resources/icons/96/@2x/wordcloud.png create mode 100644 src/test/java/com/rapidminer/connection/ConnectionInformationBuilderTest.java create mode 100644 src/test/java/com/rapidminer/connection/ConnectionInformationFileUtilsTest.java create mode 100644 src/test/java/com/rapidminer/connection/adapter/ConnectionAdapterHandlerTest.java create mode 100644 src/test/java/com/rapidminer/connection/configuration/ConfigurationParameterImplTest.java create mode 100644 src/test/java/com/rapidminer/connection/configuration/ConnectionConfigurationBuilderTest.java create mode 100644 src/test/java/com/rapidminer/connection/configuration/ConnectionResources.java create mode 100644 src/test/java/com/rapidminer/connection/gui/actions/ParameterInjectionDialogExample.java create mode 100644 src/test/java/com/rapidminer/connection/gui/model/ConnectionModelConverterTest.java create mode 100644 src/test/java/com/rapidminer/connection/gui/model/ConnectionParameterGroupModelTest.java create mode 100644 src/test/java/com/rapidminer/connection/gui/model/ConnectionParameterModelTest.java create mode 100644 src/test/java/com/rapidminer/connection/gui/model/MyConnectionParameterModel.java create mode 100644 src/test/java/com/rapidminer/connection/legacy/ConversionServiceTest.java create mode 100644 src/test/java/com/rapidminer/connection/util/TestExecutionContextImplTest.java create mode 100644 src/test/java/com/rapidminer/connection/util/ValidationResultTest.java create mode 100644 src/test/java/com/rapidminer/connection/valueprovider/ValueProviderParameterImplTest.java create mode 100644 src/test/java/com/rapidminer/connection/valueprovider/ValueProviderParameterTest.java create mode 100644 src/test/java/com/rapidminer/connection/valueprovider/ValueProviderUtilsTest.java create mode 100644 src/test/java/com/rapidminer/connection/valueprovider/handler/BaseValueProviderHandlerTest.java create mode 100644 src/test/java/com/rapidminer/connection/valueprovider/handler/ChainingValueProviderHandlerTest.java create mode 100644 src/test/java/com/rapidminer/connection/valueprovider/handler/MacroValueProviderHandlerTest.java create mode 100644 src/test/java/com/rapidminer/connection/valueprovider/handler/ValueProviderHandlerRegistryTest.java create mode 100644 src/test/java/com/rapidminer/gui/properties/MessageTruncationTest.java create mode 100644 src/test/java/com/rapidminer/operator/ports/metadata/ConnectionInformationMetaDataTest.java create mode 100644 src/test/java/com/rapidminer/parameter/ParameterTypeRenameAndReplaceTest.java create mode 100644 src/test/java/com/rapidminer/repository/local/LocalRepositoryFolderTest.java create mode 100644 src/test/java/com/rapidminer/repository/resource/ResourceFolderTest.java create mode 100644 src/test/java/com/rapidminer/tools/ListenerToolsTest.java create mode 100644 src/test/java/com/rapidminer/tools/ValidationUtilTest.java create mode 100644 src/test/java/com/rapidminer/tools/expression/internal/function/AntlrParserAttributeEvalTest.java create mode 100644 src/test/resources/com/rapidminer/connection/README create mode 100644 src/test/resources/com/rapidminer/connection/empty.jar create mode 100644 src/test/resources/com/rapidminer/connection/empty.txt create mode 100644 src/test/resources/com/rapidminer/connection/encoding-test.txt create mode 100644 src/test/resources/com/rapidminer/repository/resource/README.txt create mode 100644 src/test/resources/com/rapidminer/resources/resourcerepositorytest/CONTENTS create mode 100644 src/test/resources/com/rapidminer/resources/resourcerepositorytest/Connections/CONTENTS create mode 100644 src/test/resources/com/rapidminer/resources/resourcerepositorytest/Connections/one.conninfo diff --git a/build.gradle b/build.gradle index b56bde844..050a8e9c2 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { dependencies { // belt project for new data core - compile 'com.rapidminer:belt:0.4' + compile 'com.rapidminer:belt:1.0.0-BETA' // belt adapter for conversion between old and new core compile ('com.rapidminer:belt-adapter:0.3'){ @@ -31,8 +31,8 @@ dependencies { compile 'com.rapidminer.studio:rapidminer-studio-osx-adapter:1.0.2' // RapidMiner license framework for license management - compile 'com.rapidminer.license:rapidminer-license-api:4.0.5' - compile('com.rapidminer.license:rapidminer-license-commons:4.0.5'){ + compile 'com.rapidminer.license:rapidminer-license-api:4.1.2' + compile('com.rapidminer.license:rapidminer-license-commons:4.1.2'){ exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core' exclude group: 'com.fasterxml.jackson.core', module: 'jackson-databind' } diff --git a/gradle.properties b/gradle.properties index 7aac76a16..17ed4cf89 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=9.2.0 +version=9.3.0 group=com.rapidminer.studio diff --git a/src/generated/java/com/rapid_i/repository/wsimport/EntryResponse.java b/src/generated/java/com/rapid_i/repository/wsimport/EntryResponse.java index 01dd1d4fc..0fea60555 100644 --- a/src/generated/java/com/rapid_i/repository/wsimport/EntryResponse.java +++ b/src/generated/java/com/rapid_i/repository/wsimport/EntryResponse.java @@ -38,6 +38,7 @@ * <element name="latestRevision" type="{http://www.w3.org/2001/XMLSchema}int"/> * <element name="location" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/> * <element name="size" type="{http://www.w3.org/2001/XMLSchema}int"/> + * <element name="sizeLong" type="{http://www.w3.org/2001/XMLSchema}long" minOccurs="0"/> * <element name="type" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/> * <element name="user" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/> * </sequence> @@ -55,6 +56,7 @@ "latestRevision", "location", "size", + "sizeLong", "type", "user" }) @@ -67,6 +69,7 @@ public class EntryResponse protected int latestRevision; protected String location; protected int size; + protected Long sizeLong; protected String type; protected String user; @@ -166,6 +169,30 @@ public void setSize(int value) { this.size = value; } + /** + * Gets the value of the sizeLong property. + * + * @return + * possible object is + * {@link Long } + * + */ + public Long getSizeLong() { + return sizeLong; + } + + /** + * Sets the value of the sizeLong property. + * + * @param value + * allowed object is + * {@link Long } + * + */ + public void setSizeLong(Long value) { + this.sizeLong = value; + } + /** * Gets the value of the type property. * diff --git a/src/main/java/com/rapidminer/Process.java b/src/main/java/com/rapidminer/Process.java index 0c3061a1e..dd20e1837 100644 --- a/src/main/java/com/rapidminer/Process.java +++ b/src/main/java/com/rapidminer/Process.java @@ -27,8 +27,6 @@ import java.io.StringReader; import java.net.URL; import java.nio.charset.Charset; -import java.nio.charset.IllegalCharsetNameException; -import java.nio.charset.UnsupportedCharsetException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -44,6 +42,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; +import java.util.stream.Collectors; import javax.swing.event.EventListenerList; import org.w3c.dom.Document; @@ -97,10 +96,10 @@ import com.rapidminer.tools.AbstractObservable; import com.rapidminer.tools.LogService; import com.rapidminer.tools.LoggingHandler; -import com.rapidminer.tools.Observable; import com.rapidminer.tools.Observer; import com.rapidminer.tools.OperatorService; import com.rapidminer.tools.ParameterService; +import com.rapidminer.tools.ProcessTools; import com.rapidminer.tools.ProgressListener; import com.rapidminer.tools.RandomGenerator; import com.rapidminer.tools.ResultService; @@ -226,11 +225,11 @@ public class Process extends AbstractObservable implements Cloneable { /** Indicates whether we are updating meta data. */ private transient DebugMode debugMode = DebugMode.DEBUG_OFF; - private transient final Logger logger = makeLogger(); + private final transient Logger logger = makeLogger(); /** @deprecated Use {@link #getLogger()} */ @Deprecated - private transient final LoggingHandler logService = new WrapperLoggingHandler(logger); + private final transient LoggingHandler logService = new WrapperLoggingHandler(logger); private ProcessContext context = new ProcessContext(); @@ -535,10 +534,8 @@ public void addDataTable(final DataTable table) { /** Clears a single data table, i.e. removes all entries. */ public void clearDataTable(final String name) { DataTable table = getDataTable(name); - if (table != null) { - if (table instanceof SimpleDataTable) { - ((SimpleDataTable) table).clear(); - } + if (table instanceof SimpleDataTable) { + ((SimpleDataTable) table).clear(); } } @@ -652,11 +649,7 @@ public Collection getAllOperators() { /** Returns a Set view of all operator names (i.e. Strings). */ public Collection getAllOperatorNames() { - Collection allNames = new LinkedList<>(); - for (Operator o : getAllOperators()) { - allNames.add(o.getName()); - } - return allNames; + return getAllOperators().stream().map(Operator::getName).collect(Collectors.toList()); } /** Sets the operator that is currently being executed. */ @@ -1088,10 +1081,8 @@ protected void saveResults() throws UserError { RepositoryLocation location; try { location = rootOperator.getProcess().resolveRepositoryLocation(locationStr); - } catch (MalformedRepositoryLocationException e1) { - throw new PortUserError(port, 325, e1.getMessage()); - } catch (UserError e1) { - throw new PortUserError(port, 325, e1.getMessage()); + } catch (MalformedRepositoryLocationException | UserError e) { + throw new PortUserError(port, 325, e.getMessage()); } IOObject data = port.getDataOrNull(IOObject.class); if (data == null) { @@ -1476,10 +1467,6 @@ public static Charset getEncoding(String encoding) { } else { try { result = Charset.forName(encoding); - } catch (IllegalCharsetNameException e) { - result = Charset.defaultCharset(); - } catch (UnsupportedCharsetException e) { - result = Charset.defaultCharset(); } catch (IllegalArgumentException e) { result = Charset.defaultCharset(); } @@ -1580,23 +1567,9 @@ public void readProcess(final Reader in, final ProgressListener progressListener * as operator name. */ public String registerName(final String name, final Operator operator) { - if (operatorNameMap.get(name) != null) { - String baseName = name; - int index = baseName.indexOf(" ("); - if (index >= 0) { - baseName = baseName.substring(0, index); - } - int i = 2; - while (operatorNameMap.get(baseName + " (" + i + ")") != null) { - i++; - } - String newName = baseName + " (" + i + ")"; - operatorNameMap.put(newName, operator); - return newName; - } else { - operatorNameMap.put(name, operator); - return name; - } + String newName = ProcessTools.getNewName(operatorNameMap.keySet(), name); + operatorNameMap.put(newName, operator); + return newName; } /** This method is used for unregistering a name from the operator name map. */ @@ -1608,6 +1581,26 @@ public void notifyRenaming(final String oldName, final String newName) { rootOperator.notifyRenaming(oldName, newName); } + /** + * This method is called when the operator given by {@code oldName} (and {@code oldOp} if it is not {@code null}) + * was replaced with the operator described by {@code newName} and {@code newOp}. + * This will inform the {@link ProcessRootOperator} of the replacing. + * + * @param oldName + * the name of the old operator + * @param oldOp + * the old operator; can be {@code null} + * @param newName + * the name of the new operator + * @param newOp + * the new operator; must not be {@code null} + * @see Operator#notifyReplacing(String, Operator, String, Operator) + * @since 9.3 + */ + public void notifyReplacing(String oldName, Operator oldOp, String newName, Operator newOp) { + rootOperator.notifyReplacing(oldName, oldOp, newName, newOp); + } + @Override public String toString() { if (rootOperator == null) { @@ -1620,20 +1613,8 @@ public String toString() { private final EventListenerList processSetupListeners = new EventListenerList(); /** Delegates any changes in the ProcessContext to the root operator. */ - private final Observer delegatingContextObserver = new Observer() { - - @Override - public void update(final Observable observable, final ProcessContext arg) { - fireUpdate(); - } - }; - private final Observer delegatingOperatorObserver = new Observer() { - - @Override - public void update(final Observable observable, final Operator arg) { - fireUpdate(); - } - }; + private final Observer delegatingContextObserver = (observable, arg) -> fireUpdate(); + private final Observer delegatingOperatorObserver = (observable, arg) -> fireUpdate(); public void addProcessSetupListener(final ProcessSetupListener listener) { processSetupListeners.add(ProcessSetupListener.class, listener); diff --git a/src/main/java/com/rapidminer/RapidMiner.java b/src/main/java/com/rapidminer/RapidMiner.java index a59e4c0aa..7a8552ad8 100644 --- a/src/main/java/com/rapidminer/RapidMiner.java +++ b/src/main/java/com/rapidminer/RapidMiner.java @@ -37,6 +37,7 @@ import java.util.Set; import java.util.logging.Level; +import com.rapidminer.connection.ConnectionInformationFileUtils; import com.rapidminer.core.license.ActionStatisticsLicenseManagerListener; import com.rapidminer.core.license.ProductConstraintManager; import com.rapidminer.gui.RapidMinerGUI; @@ -698,6 +699,10 @@ public static void init(final Product product, final LicenseLocation licenseLoca performInitialSettings(); ParameterService.init(); ParameterService.setParameterValue(PROPERTY_RAPIDMINER_VERSION, RapidMiner.getLongVersion()); + if (getExecutionMode().canAccessFilesystem()) { + ConnectionInformationFileUtils.checkForCacheClearing(); + ConnectionInformationFileUtils.initSettings(); + } // initializing networking tools GlobalAuthenticator.init(); diff --git a/src/main/java/com/rapidminer/connection/ConnectionHandler.java b/src/main/java/com/rapidminer/connection/ConnectionHandler.java new file mode 100644 index 000000000..9759cce5a --- /dev/null +++ b/src/main/java/com/rapidminer/connection/ConnectionHandler.java @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection; + +import java.util.Collections; +import java.util.Map; + +import com.rapidminer.connection.util.GenericHandler; + + +/** + * An interface for handler/factory for {@link ConnectionInformation ConnectionInformations}. Implementations provide + * the possibility to create new {@link ConnectionInformation ConnectionInformations}. They can be registered using + * {@link ConnectionHandlerRegistry#registerHandler(GenericHandler) ConnectionHandlerRegistry.registerHandler}. + * Additionally implementations can provide a set of additional actions for the connections. + * + * @author Jan Czogalla + * @since 9.3 + * @see com.rapidminer.tools.config.ConfigurableConnectionHandler ConfigurableConnectionHandler + */ +public interface ConnectionHandler extends GenericHandler { + + /** + * Creates a new instance of {@link ConnectionInformation} with the given name, this handler's type + * and an implementation dependent id. + * + * @param name + * the name of the new connection; must not be {@code null} + * @see ConnectionInformationBuilder + */ + ConnectionInformation createNewConnectionInformation(String name); + + /** + * A map of name or key/runnable pairs, representing additional actions + * + * @return an empty map by default + */ + default Map getAdditionalActions() { + return Collections.emptyMap(); + } +} diff --git a/src/main/java/com/rapidminer/connection/ConnectionHandlerRegistry.java b/src/main/java/com/rapidminer/connection/ConnectionHandlerRegistry.java new file mode 100644 index 000000000..d29ddc61b --- /dev/null +++ b/src/main/java/com/rapidminer/connection/ConnectionHandlerRegistry.java @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection; + +import com.rapidminer.connection.util.GenericHandlerRegistry; +import com.rapidminer.connection.util.GenericRegistrationEventListener; +import com.rapidminer.operator.OperatorVersion; + + +/** + * Registry for {@link ConnectionHandler ConnectionHandlers}. Handlers can be registered and unregistered, + * searched by type and (un)registrations can be observed. See {@link GenericHandlerRegistry} for further details. + * + * @author Jan Czogalla + * @since 9.3 + */ +public final class ConnectionHandlerRegistry extends GenericHandlerRegistry { + + public static final OperatorVersion BEFORE_NEW_CONNECTION_MANAGEMENT = new OperatorVersion(9, 2, 1); + + private static final ConnectionHandlerRegistry INSTANCE = new ConnectionHandlerRegistry(); + + /** + * Singleton class, no instantiation allowed except for internal purpose + */ + private ConnectionHandlerRegistry() { + } + + /** + * Get the instance of this singleton + */ + public static ConnectionHandlerRegistry getInstance() { + return INSTANCE; + } + + @Override + @SuppressWarnings("unchecked") + protected , L extends G> Class getListenerClass(L listener) { + return (Class) (listener == null || listener instanceof ConnectionHandlerRegistryListener ? + ConnectionHandlerRegistryListener.class : listener.getClass()); + } + + @Override + protected String getRegistryType() { + return "connection"; + } +} diff --git a/src/main/java/com/rapidminer/connection/ConnectionHandlerRegistryListener.java b/src/main/java/com/rapidminer/connection/ConnectionHandlerRegistryListener.java new file mode 100644 index 000000000..429577676 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/ConnectionHandlerRegistryListener.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection; + +import com.rapidminer.connection.util.GenericRegistrationEventListener; + +/** + * Marker interface for registration events happening in the {@link ConnectionHandlerRegistry}. + * + * @author Jan Czogalla + * @since 9.3 + */ +public interface ConnectionHandlerRegistryListener extends GenericRegistrationEventListener {} diff --git a/src/main/java/com/rapidminer/connection/ConnectionInformation.java b/src/main/java/com/rapidminer/connection/ConnectionInformation.java new file mode 100644 index 000000000..d175f0107 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/ConnectionInformation.java @@ -0,0 +1,82 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +import com.rapidminer.connection.configuration.ConnectionConfiguration; +import com.rapidminer.operator.Annotations; +import com.rapidminer.repository.Repository; + + +/** + * Interface for connection information. Instances contain everything that is needed to connect, according to the + * {@link ConnectionHandler} for the {@link ConnectionConfiguration ConnectionConfiguration's} type. + *

+ * The {@link ConnectionConfiguration} is mandatory, a default {@link ConnectionStatistics} object is present and can be updated. + * If necessary, the instance contains also a list of library and/or other files. + *

+ * Everything is nicely zipped up in a single file. New connection information objects can be created using the {@link ConnectionInformationBuilder}. + * + * @author Jan Czogalla + * @since 9.3 + */ +public interface ConnectionInformation { + + /** Internal name of the {@link ConnectionConfiguration} */ + String ENTRY_NAME_CONFIG = "Config"; + /** Internal name of the {@link ConnectionStatistics} */ + String ENTRY_NAME_STATS = "Stats"; + /** Internal name of the {@link Annotations} */ + String ENTRY_NAME_ANNOTATIONS = "Annotations"; + /** Internal name of the library file dir */ + String DIRECTORY_NAME_LIB = "Lib"; + /** Internal name of the general file dir */ + String DIRECTORY_NAME_FILES = "Files"; + + /** Gets the connection configuration */ + ConnectionConfiguration getConfiguration(); + + /** Gets the connection statistics if present */ + ConnectionStatistics getStatistics(); + + /** Gets a (possibly empty) list of library files */ + List getLibraryFiles(); + + /** Gets a (possibly empty) list of general files */ + List getOtherFiles(); + + /** Gets the Annotations from this ConnectionInformation */ + Annotations getAnnotations(); + + /** Returns the {@link Repository} this connection belongs to. Might be {@code null} */ + Repository getRepository(); + + /** Create a carbon copy of this connection */ + default ConnectionInformation copy() { + try { + return new ConnectionInformationBuilder(this).build(); + } catch (IOException e) { + // should not happen; see ConnectionConfigurationBuilder(ConnectionConfiguration) + return null; + } + } +} diff --git a/src/main/java/com/rapidminer/connection/ConnectionInformationBuilder.java b/src/main/java/com/rapidminer/connection/ConnectionInformationBuilder.java new file mode 100644 index 000000000..8c552fd6b --- /dev/null +++ b/src/main/java/com/rapidminer/connection/ConnectionInformationBuilder.java @@ -0,0 +1,181 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.rapidminer.connection.configuration.ConnectionConfiguration; +import com.rapidminer.connection.configuration.ConnectionConfigurationBuilder; +import com.rapidminer.tools.ValidationUtil; +import com.rapidminer.operator.Annotations; +import com.rapidminer.repository.Repository; + + +/** + * Builder for {@link ConnectionInformation}. Can create a new instance based on an existing {@link ConnectionInformation} + * or an instance of {@link ConnectionConfiguration}. + * + * @author Jan Czogalla, Andreas Timm + * @since 9.3 + */ +public class ConnectionInformationBuilder { + + private boolean isUpdatable = false; + + private ConnectionConfiguration configuration; + private ConnectionStatistics statistics; + private Annotations annotations; + private List libraryFiles; + private List otherFiles; + private Repository repository; + + /** + * Create a builder based on an existing {@link ConnectionInformation} + */ + public ConnectionInformationBuilder(ConnectionInformation original) throws IOException { + ValidationUtil.requireNonNull(original, "original connection information"); + isUpdatable = true; + this.configuration = new ConnectionConfigurationBuilder(original.getConfiguration()).build(); + this.statistics = new ConnectionStatisticsBuilder(original.getStatistics()).build(); + this.annotations = new Annotations(original.getAnnotations()); + this.libraryFiles = new ArrayList<>(original.getLibraryFiles()); + this.otherFiles = new ArrayList<>(original.getOtherFiles()); + this.repository = original.getRepository(); + } + + /** + * Create a builder based on a {@link ConnectionConfiguration} + * + * @param configuration + * the original configuration; must not be {@code null} + */ + public ConnectionInformationBuilder(ConnectionConfiguration configuration) { + this.configuration = ValidationUtil.requireNonNull(configuration, "configuration"); + } + + /** + * Update the {@link ConnectionConfiguration} if this builder was created with {@link #ConnectionInformationBuilder(ConnectionInformation)} + * + * @param configuration + * the updated configuration; must not be {@code null} + */ + public ConnectionInformationBuilder updateConnectionConfiguration(ConnectionConfiguration configuration) { + if (!isUpdatable) { + throw new IllegalArgumentException("Cannot update a new Connection Information object"); + } + this.configuration = ValidationUtil.requireNonNull(configuration, "configuration"); + return this; + } + + /** + * Sets the repository that this connection will be saved in. + * + * @param repository + * the repository, must not be {@code null} + */ + public ConnectionInformationBuilder inRepository(Repository repository) { + this.repository = ValidationUtil.requireNonNull(repository, "repository"); + return this; + } + + /** + * Add/overwrite the {@link ConnectionStatistics} for the {@link ConnectionInformation} + * + * @param statistics + * the statistics; must not be {@code null} + */ + public ConnectionInformationBuilder withStatistics(ConnectionStatistics statistics) { + this.statistics = ValidationUtil.requireNonNull(statistics, "statistics"); + return this; + } + + /** + * Add/overwrite the library files for the {@link ConnectionInformation}. Throws an error on non-existing entries. + * + * @param libraryFiles + * the list of library files; can be {@code null} or empty; all non-{@code null} elements must exist + */ + public ConnectionInformationBuilder withLibraryFiles(List libraryFiles) { + libraryFiles = ValidationUtil.stripToEmptyList(libraryFiles); + List nonExistent = libraryFiles.stream().filter(path -> !Files.exists(path)).collect(Collectors.toList()); + if (!nonExistent.isEmpty()) { + throw new IllegalArgumentException("Non-existing paths found: " + nonExistent); + } + this.libraryFiles = libraryFiles; + return this; + } + + /** + * Add/overwrite the general files for the {@link ConnectionInformation}. Throws an error on non-existing entries. + * + * @param otherFiles + * the list of other files; can be {@code null} or empty; all non-{@code null} elements must exist + */ + public ConnectionInformationBuilder withOtherFiles(List otherFiles) { + otherFiles = ValidationUtil.stripToEmptyList(otherFiles); + List nonExistent = otherFiles.stream().filter(path -> !Files.exists(path)).collect(Collectors.toList()); + if (!nonExistent.isEmpty()) { + throw new IllegalArgumentException("Non-existing paths found: " + nonExistent); + } + this.otherFiles = otherFiles; + return this; + } + + /** + * Add these {@link Annotations} already. + * + * @param annotations + * to be added to this {@link ConnectionInformation} + */ + public ConnectionInformationBuilder withAnnotations(Annotations annotations) { + this.annotations = annotations; + return this; + } + + /** + * Creates the final {@link ConnectionInformation} with the content added through this builder. + */ + public ConnectionInformation build() { + ConnectionInformationImpl ci = new ConnectionInformationImpl(); + ci.configuration = configuration; + if (statistics != null) { + ci.statistics = statistics; + } + if (libraryFiles != null) { + ci.libraryFiles = libraryFiles; + } + if (otherFiles != null) { + ci.otherFiles = otherFiles; + } + if (annotations != null) { + ci.annotations = annotations; + } + if (repository != null) { + ci.repository = repository; + } + return ci; + } + + +} diff --git a/src/main/java/com/rapidminer/connection/ConnectionInformationContainerIOObject.java b/src/main/java/com/rapidminer/connection/ConnectionInformationContainerIOObject.java new file mode 100644 index 000000000..10465a73d --- /dev/null +++ b/src/main/java/com/rapidminer/connection/ConnectionInformationContainerIOObject.java @@ -0,0 +1,117 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection; + +import java.io.IOException; + +import com.rapidminer.connection.configuration.ConnectionConfiguration; +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.operator.Annotations; +import com.rapidminer.operator.ResultObjectAdapter; +import com.rapidminer.repository.Repository; +import com.rapidminer.repository.RepositoryLocation; +import com.rapidminer.tools.I18N; +import com.rapidminer.tools.Tools; + + +/** + * Container to pass the {@link ConnectionInformation} around as an {@link com.rapidminer.operator.IOObject IOObject}. + * + * @author Andreas Timm + * @since 9.3 + */ +public class ConnectionInformationContainerIOObject extends ResultObjectAdapter { + + // the contained connection information + private transient ConnectionInformation connectionInformation; + + /** + * Set this container up with the given {@link ConnectionInformation}. + */ + public ConnectionInformationContainerIOObject(ConnectionInformation connectionInformation) { + this.connectionInformation = connectionInformation; + } + + /** + * Access the {@link ConnectionInformation} + * + * @return the connection information + */ + public ConnectionInformation getConnectionInformation() { + return connectionInformation; + } + + @Override + public Annotations getAnnotations() { + return connectionInformation.getAnnotations(); + } + + @Override + public ConnectionInformationContainerIOObject copy() { + ConnectionInformationBuilder copyBuilder; + try { + copyBuilder = new ConnectionInformationBuilder(connectionInformation); + } catch (IOException e) { + return null; + } + return new ConnectionInformationContainerIOObject(copyBuilder.build()); + } + + @Override + public String toString() { + ConnectionConfiguration configuration = connectionInformation.getConfiguration(); + if (configuration == null) { + return "Empty connection"; + } + return "Connection: " + configuration.getName() + " of type " + configuration.getType(); + } + + @Override + public String toResultString() { + ConnectionConfiguration configuration = connectionInformation.getConfiguration(); + if (configuration == null) { + return "Empty connection"; + } + String result = "Name: " + configuration.getName() + "

Type: "; + String connectionType = configuration.getType(); + boolean typeKnown = ConnectionHandlerRegistry.getInstance().isTypeKnown(connectionType); + if (typeKnown) { + String icon = ConnectionI18N.getConnectionIconName(connectionType); + java.net.URL url = Tools.getResource("icons/16/" + icon); + if (url != null) { + result += " "; + } + result += ConnectionI18N.getTypeName(configuration.getType()); + } else { + result += I18N.getGUILabel("connection.unknown_type.label"); + } + result += "

Location: " + RepositoryLocation.REPOSITORY_PREFIX; + Repository repository = connectionInformation.getRepository(); + if (repository == null) { + return result; + } + return result + repository.getName(); + } + + @Override + public String getName() { + return "Connection"; + } + +} diff --git a/src/main/java/com/rapidminer/connection/ConnectionInformationFileUtils.java b/src/main/java/com/rapidminer/connection/ConnectionInformationFileUtils.java new file mode 100644 index 000000000..67a19b8b5 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/ConnectionInformationFileUtils.java @@ -0,0 +1,363 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection; + +import java.awt.event.ActionEvent; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; + +import com.rapidminer.RapidMiner; +import com.rapidminer.gui.RapidMinerGUI; +import com.rapidminer.gui.tools.ResourceAction; +import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.gui.tools.dialogs.ConfirmDialog; +import com.rapidminer.parameter.ParameterType; +import com.rapidminer.parameter.ParameterTypeCategory; +import com.rapidminer.parameter.ParameterTypeLinkButton; +import com.rapidminer.parameter.ParameterTypeTupel; +import com.rapidminer.tools.FileSystemService; +import com.rapidminer.tools.ListenerTools; +import com.rapidminer.tools.LogService; +import com.rapidminer.tools.ParameterService; + + +/** + * Util method collection for loading, copying and moving {@link ConnectionInformation} + * + * @author Jan Czogalla, Andreas Timm + * @since 9.3 + */ +public final class ConnectionInformationFileUtils { + + /** + * {@link java.nio.file.FileVisitor} that creates or resolves cache files. + * The cache is created in the given target, mirroring the directory structure and saving each file inside the directory + * {@code /}. For files that do not have a corresponding {@code .md5} path the hash + * will be calculated if possible, otherwise they will be ignored. + * + * @author Jan Czogalla + * @since 9.3 + */ + private static final class CachingFileVisitor extends SimpleFileVisitor { + + private final Path source; + private final Path target; + private final List fileList; + + private CachingFileVisitor(Path source, Path target, List fileList) { + if (source.toFile().isFile()) { + source = source.getParent(); + } + this.source = source; + this.target = target; + this.fileList = fileList; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if ((attrs == null ? file.toFile().isDirectory() : attrs.isDirectory()) || file.getFileName().endsWith(ConnectionInformationSerializer.MD5_SUFFIX)) { + return FileVisitResult.CONTINUE; + } + // check if it already is a cache file + if (file.startsWith(target.toString())) { + return FileVisitResult.CONTINUE; + } + String md5Hash = getMD5Hex(file); + if (md5Hash == null) { + // skip files without valid md5 + return FileVisitResult.CONTINUE; + } + Path relative = source.relativize(file); + // resolve cached file location + Path cacheLocation = target.resolve(Paths.get(relative.toString(), md5Hash, file.getFileName().toString())); + if (cacheLocation.toFile().exists()) { + // cache exists, collect + fileList.add(cacheLocation); + return FileVisitResult.CONTINUE; + } + // create new cached file + Files.createDirectories(cacheLocation.getParent()); + fileList.add(Files.copy(file, cacheLocation)); + return FileVisitResult.CONTINUE; + } + + /** + * Get md5 hash from sibling .md5 file or calculate it using {@link DigestUtils#md5Hex(InputStream)} + */ + private String getMD5Hex(Path file) { + if (!file.toFile().exists()) { + return null; + } + Path md5File = file.getParent().resolve(file.getFileName() + ConnectionInformationSerializer.MD5_SUFFIX); + if (md5File.toFile().exists()) { + // read md5 hash from file if possible + try (BufferedReader br = Files.newBufferedReader(md5File)) { + return StringUtils.trimToNull(br.readLine()); + } catch (IOException e) { + // ignore, handling below + } + } + // calculate md5 hash + try (InputStream is = Files.newInputStream(file)) { + return DigestUtils.md5Hex(is); + } catch (IOException e) { + // ignore + } + return null; + } + } + + private static final String KEY_CLEAR_CACHE_NOW = "connection.clear_cache_now"; + private static final String PARAMETER_KEEP_CACHE = "rapidminer.system.file_cache.connection.keep"; + private static final String PARAMETER_CLEAR_CACHE_ONCE = "rapidminer.system.file_cache.connection.clear_once"; + + private static final String STRATEGY_INDEFINITELY = "indefinitely"; + private static final String[] KEEP_CACHE_STRATEGIES = {STRATEGY_INDEFINITELY, "never"}; + + /** Action to force clearing the cache and restarting Rapidminer */ + private static final ResourceAction CLEAR_CACHE_NOW_ACTION = new ResourceAction(KEY_CLEAR_CACHE_NOW) { + @Override + protected void loggedActionPerformed(ActionEvent e) { + int result = SwingTools.showConfirmDialog(KEY_CLEAR_CACHE_NOW, ConfirmDialog.OK_CANCEL_OPTION); + if (result == ConfirmDialog.CANCEL_OPTION) { + return; + } + RapidMiner.addShutdownHook(ConnectionInformationFileUtils::clearCache); + ParameterService.setParameterValue(PARAMETER_CLEAR_CACHE_ONCE, "true"); + ParameterService.saveParameters(); + RapidMinerGUI.getMainFrame().exit(true); + } + }; + + /** + * Utility class. + */ + private ConnectionInformationFileUtils() { + throw new AssertionError("Do not instantiate this utility class"); + } + + /** Initialize the {@link ParameterType} for the {@value #PARAMETER_KEEP_CACHE} setting. */ + public static void initSettings() { + ParameterType cacheType = new ParameterTypeTupel(PARAMETER_KEEP_CACHE, "", + new ParameterTypeCategory("keep_strategy", "", KEEP_CACHE_STRATEGIES, 0), + new ParameterTypeLinkButton("clear_cache", "", CLEAR_CACHE_NOW_ACTION)); + RapidMiner.registerParameter(cacheType, "system"); + } + + /** + * Checks if the cache should be cleared using the parameters {@value #PARAMETER_KEEP_CACHE} and {@value #PARAMETER_CLEAR_CACHE_ONCE}. + * If the cache should not be kept or if it should be cleared once, {@link #clearCache()} is called to clean up. + * This should only be called during Studio startup to prevent breaking class loaders and others. + */ + public static void checkForCacheClearing() { + String keepCacheValue = ParameterService.getParameterValue(PARAMETER_KEEP_CACHE); + keepCacheValue = ParameterTypeTupel.transformString2Tupel(keepCacheValue)[0]; + boolean keepCache = keepCacheValue == null || keepCacheValue.equals(STRATEGY_INDEFINITELY); + boolean clearOnce = Boolean.parseBoolean(ParameterService.getParameterValue(PARAMETER_CLEAR_CACHE_ONCE)); + if (!keepCache || clearOnce) { + clearCache(); + } + if (clearOnce) { + ParameterService.setParameterValue(PARAMETER_CLEAR_CACHE_ONCE, "false"); + ParameterService.saveParameters(); + } + } + + /** + * Turns all regular files in the given path into {@link Path Paths} in the file system, + * by either creating a cached version of the file or resolving it to an existing cached file. + * The cache is created in the user's .RapidMiner directory. + * + * @param path + * the path to a single file to cache or a subfolder to cache all children + * @see CachingFileVisitor + */ + public static List getOrCreateCacheFiles(Path path) throws IOException { + Path cacheBaseLocation = getCacheLocation(); + List fileList = new ArrayList<>(); + if (path.toFile().exists()) { + Files.walkFileTree(path, Collections.emptySet(), 1, new CachingFileVisitor(path, cacheBaseLocation, fileList)); + } + return fileList; + } + + /** + * Saves this connection information after updating either the configuration or statistics + *

+ * Note: This might overwrite changes done in the file system. + */ + public static void save(ConnectionInformation connectionInformation, Path zipFile) throws IOException { + if (zipFile == null) { + throw new NoSuchFileException("Target file was not set"); + } + try (OutputStream out = new FileOutputStream(zipFile.toFile())) { + ConnectionInformationSerializer.LOCAL.serialize(connectionInformation, out); + } + } + + /** + * Moves this connection information to the designated new location. + *

+ * Note: This will overwrite an existing file without asking. Make sure to check for overwriting first. + * + * @param target + * the target {@link Path}; must not be {@code null} + */ + public static void moveTo(Path sourcePath, Path target) throws IOException { + Files.move(sourcePath, target, StandardCopyOption.REPLACE_EXISTING); + } + + + /** + * Creates and returns a copy of this connection information at the designated new location. + *

+ * Note: This will overwrite an existing file without asking. Make sure to check for overwriting first. + * + * @param target + * the target path; must not be {@code null} + */ + public static void copyTo(Path source, Path target) throws IOException { + Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); + } + + + /** + * Copies the given files to the specified {@code root/dirName} and creates {@code .md5} hash files for each. + */ + static void copyFilesToZip(Path root, List filesToCopy, String dirName) throws IOException { + if (filesToCopy == null || filesToCopy.isEmpty() || dirName == null) { + return; + } + Path path = root.resolve(dirName); + Files.createDirectory(path); + MessageDigest md5 = DigestUtils.getMd5Digest(); + for (Path fileToCopy : filesToCopy) { + Path fileName = fileToCopy.getFileName(); + Path nestedFilePath = path.resolve(fileName.toString()); + try (InputStream is = Files.newInputStream(fileToCopy); + DigestInputStream dis = new DigestInputStream(is, md5); + BufferedWriter md5Writer = Files.newBufferedWriter(path.resolve(fileName + ConnectionInformationSerializer.MD5_SUFFIX))) { + Files.copy(dis, nestedFilePath); + md5Writer.write(Hex.encodeHexString(md5.digest())); + } + } + } + + /** + * Load a {@link ConnectionInformation} from an existing zip file. + * + * @param zipFile + * the zip file to load from; must not be {@code null} + */ + public static ConnectionInformation loadFromZipFile(Path zipFile) throws IOException { + return ConnectionInformationSerializer.LOCAL.loadConnection(new FileInputStream(zipFile.toFile())); + } + + private static Path getCacheLocation() { + return FileSystemService.getUserRapidMinerDir().toPath().resolve(FileSystemService.RAPIDMINER_INTERNAL_CACHE_CONNECTION_FULL); + } + + /** + * Internally store the file in a cached folder, will not overwrite existing + * + * @param name + * of the file to be added + * @param inputStream + * source stream of the file + * @param md5Hash + * of the file, used for caching, will be calculated if missing + */ + static Path addFileInternally(String name, InputStream inputStream, String md5Hash) throws IOException { + final MessageDigest msgDigest = DigestUtils.getMd5Digest(); + inputStream = new DigestInputStream(inputStream, msgDigest); + final Path tempFile = Files.createTempFile("conninfo", "tmp"); + try (FileOutputStream fos = new FileOutputStream(tempFile.toFile())) { + IOUtils.copy(inputStream, fos); + } + String testHash = Hex.encodeHexString(msgDigest.digest()); + tempFile.toFile().deleteOnExit(); + + if (md5Hash == null) { + md5Hash = testHash; + } else if (!md5Hash.equals(testHash)) { + silentDelete(tempFile); + throw new IOException("Mismatched md5 hash!"); + } + + final Path cacheLocation = ConnectionInformationFileUtils.getCacheLocation().resolve(name).resolve(md5Hash).resolve(name); + + if (!cacheLocation.toFile().exists()) { + try { + Files.createDirectories(cacheLocation.getParent()); + moveTo(tempFile, cacheLocation); + } catch (IOException e) { + silentDelete(tempFile); + throw e; + } + } + return cacheLocation; + } + + /** Silently delete the given path. */ + private static void silentDelete(Path tempFile) { + try { + Files.delete(tempFile); + } catch (IOException e) { + // ignore + } + } + + /** Clear the connection file cache. Will log the first occurring error and how many errors occurred in total. */ + private static void clearCache() { + try (Stream paths = Files.walk(getCacheLocation())) { + ListenerTools.informAllAndThrow(paths.sorted(Comparator.reverseOrder()).collect(Collectors.toList()), + Files::delete, IOException.class); + } catch (IOException ioe) { + Throwable[] suppressed = ioe.getSuppressed(); + LogService.getRoot().log(Level.WARNING, "com.rapidminer.connection.file_cache.unable_to_delete", new Object[]{ioe.getMessage(), suppressed.length + 1}); + } + } +} diff --git a/src/main/java/com/rapidminer/connection/ConnectionInformationImpl.java b/src/main/java/com/rapidminer/connection/ConnectionInformationImpl.java new file mode 100644 index 000000000..e2da2428d --- /dev/null +++ b/src/main/java/com/rapidminer/connection/ConnectionInformationImpl.java @@ -0,0 +1,131 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection; + + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.apache.commons.collections4.CollectionUtils; + +import com.rapidminer.connection.configuration.ConnectionConfiguration; +import com.rapidminer.operator.Annotations; +import com.rapidminer.repository.Repository; + + +/** + * Implementation of {@link ConnectionInformation}. + * + * @author Jan Czogalla + * @since 9.3 + */ +public final class ConnectionInformationImpl implements ConnectionInformation { + + // the configuration data of a connection + ConnectionConfiguration configuration; + // statistics about the usage + ConnectionStatistics statistics = new ConnectionStatisticsImpl(); + // Annotations for this object + Annotations annotations = new Annotations(); + // files for the connection + List libraryFiles = Collections.synchronizedList(new ArrayList<>()); + List otherFiles = Collections.synchronizedList(new ArrayList<>()); + // the repository of this connection + Repository repository; + + @Override + public ConnectionConfiguration getConfiguration() { + return configuration; + } + + @Override + public ConnectionStatistics getStatistics() { + return statistics; + } + + @Override + public List getLibraryFiles() { + return new ArrayList<>(libraryFiles); + } + + @Override + public List getOtherFiles() { + return new ArrayList<>(otherFiles); + } + + @Override + public Annotations getAnnotations() { + return annotations; + } + + @Override + public Repository getRepository() { + return repository; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof ConnectionInformationImpl)) { + return false; + } + ConnectionInformationImpl other = (ConnectionInformationImpl) obj; + return Objects.equals(configuration, other.configuration) && CollectionUtils.isEqualCollection(libraryFiles, other.libraryFiles) && CollectionUtils.isEqualCollection(otherFiles, other.otherFiles) && Objects.equals(annotations, other.annotations); + } + + @Override + public int hashCode() { + return Objects.hash(configuration, libraryFiles, otherFiles); + } + + /** + * Add a lib file to this connection information + * + * @param name + * of the lib file + * @param inputStream + * source stream of the lib file + * @param md5Hash + * of the lib file, used for caching + */ + public void addLibFile(String name, InputStream inputStream, String md5Hash) throws IOException { + libraryFiles.add(ConnectionInformationFileUtils.addFileInternally(name, inputStream, md5Hash)); + } + + /** + * Add other files to this connection information + * + * @param name + * of the file + * @param inputStream + * source stream of the file + * @param md5Hash + * of the file, used for caching + */ + public void addOtherFile(String name, InputStream inputStream, String md5Hash) throws IOException { + otherFiles.add(ConnectionInformationFileUtils.addFileInternally(name, inputStream, md5Hash)); + } +} \ No newline at end of file diff --git a/src/main/java/com/rapidminer/connection/ConnectionInformationSerializer.java b/src/main/java/com/rapidminer/connection/ConnectionInformationSerializer.java new file mode 100644 index 000000000..86587bfc2 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/ConnectionInformationSerializer.java @@ -0,0 +1,556 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection; + +import static com.rapidminer.connection.ConnectionInformation.DIRECTORY_NAME_FILES; +import static com.rapidminer.connection.ConnectionInformation.DIRECTORY_NAME_LIB; +import static com.rapidminer.connection.ConnectionInformation.ENTRY_NAME_ANNOTATIONS; +import static com.rapidminer.connection.ConnectionInformation.ENTRY_NAME_CONFIG; +import static com.rapidminer.connection.ConnectionInformation.ENTRY_NAME_STATS; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.IOUtils; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.rapidminer.connection.configuration.ConnectionConfiguration; +import com.rapidminer.connection.configuration.ConnectionConfigurationBuilder; +import com.rapidminer.tools.ValidationUtil; +import com.rapidminer.connection.valueprovider.ValueProviderParameterImpl; +import com.rapidminer.operator.Annotations; +import com.rapidminer.operator.ports.metadata.ConnectionInformationMetaData; +import com.rapidminer.repository.RepositoryException; +import com.rapidminer.repository.RepositoryLocation; +import com.rapidminer.tools.LogService; +import com.rapidminer.tools.NonClosingZipInputStream; + + +/** + * ConnectionInformationSerializer used for reading and writing of {@link ConnectionInformation} objects + * + * @author Jan Czogalla, Andreas Timm, Jonas Wilms-Pfau + * @since 9.3 + */ +public final class ConnectionInformationSerializer { + + /** + * Local serializer, protected by local encryption key + */ + public static final ConnectionInformationSerializer LOCAL = new ConnectionInformationSerializer(getObjectMapper()); + + /** + * Server Serializer, protected by transport security + */ + public static final ConnectionInformationSerializer REMOTE = new ConnectionInformationSerializer(getRemoteObjectMapper()); + + /** + * Default Charset used for storing + */ + static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + /** + * File extension of the MD5 hash text file for bundled files + */ + static final String MD5_SUFFIX = ".md5"; + + /** + * File separator according to .ZIP File Format Specification 4.4.17.1 and ISO/IEC 21320-1:2015 + */ + private static final char ZIP_FILE_SEPARATOR_CHAR = '/'; + + /** + * ObjectWriter, kept a cache version for faster access + */ + private final ObjectWriter objectWriter; + + /** + * Reader for ConnectionStatistics + */ + private final ObjectReader connectionStatisticsReader; + + /** + * Reader for ConnectionConfiguration + */ + private final ObjectReader connectionConfigurationReader; + + /** + * Creates a new ConnectionInformationSerializer + * + * @param objectMapper The preconfigured objectMapper + */ + private ConnectionInformationSerializer(ObjectMapper objectMapper) { + ValidationUtil.requireNonNull(objectMapper, "objectMapper"); + this.objectWriter = objectMapper.writerWithDefaultPrettyPrinter(); + ObjectReader reader = objectMapper.reader(); + this.connectionStatisticsReader = reader.withType(ConnectionStatistics.class); + this.connectionConfigurationReader = reader.withType(ConnectionConfiguration.class); + } + + /** + * Load a {@link ConnectionConfiguration} from the given {@link Reader} + * + * @param src + * the source that contains the {@link ConnectionConfiguration} + * @return a loaded {@link ConnectionConfiguration}, can be null if the src was null + * @throws IOException + * in case the {@link Reader} fails + */ + public ConnectionConfiguration loadConfiguration(Reader src) throws IOException { + if (src == null) { + return null; + } + return connectionConfigurationReader.readValue(src); + } + + /** + * Load a {@link ConnectionConfiguration} from the inputStream. + * + * @param inputStream + * @return + * @throws IOException + */ + public ConnectionConfiguration loadConfiguration(InputStream inputStream) throws IOException { + if (inputStream == null) { + return null; + } + return connectionConfigurationReader.readValue(inputStream); + } + + /** + * Load a {@link ConnectionInformation} from the given {@link InputStream} + * + * @param stream + * the {@link InputStream} to read the {@link ConnectionInformation} content from. This should be the data that + * was produced by {@link ConnectionInformationSerializer#serialize(ConnectionInformation, OutputStream)} + * @return the {@link ConnectionInformation} the stream contained + * @throws IOException + * in case of reading errors + */ + public ConnectionInformation loadConnection(InputStream stream) throws IOException { + return loadConnection(stream, null); + } + + /** + * Load a {@link ConnectionInformation} from the given {@link InputStream} and {@link RepositoryLocation}. + * The {@link ConnectionConfiguration} will get the repository location's name as its name. + * + * @param stream + * the {@link InputStream} to read the {@link ConnectionInformation} content from. This should be the data that + * was produced by {@link ConnectionInformationSerializer#serialize(ConnectionInformation, OutputStream)} + * @param repositoryLocation + * the repository location this entry belongs too; might be {@code null} + * @return the {@link ConnectionInformation} the stream contained + * @throws IOException + * in case of reading errors + */ + public ConnectionInformation loadConnection(InputStream stream, RepositoryLocation repositoryLocation) throws IOException { + ConnectionInformationImpl ci = new ConnectionInformationImpl(); + NonClosingZipInputStream zis = new NonClosingZipInputStream(stream); + // track found files, need the file's inputstream and the md5 to add it + Map md5hashForLibFile = new HashMap<>(); + Map md5hashForOtherFile = new HashMap<>(); + Map libFileStreams = new HashMap<>(); + Map otherFileStreams = new HashMap<>(); + + for (ZipEntry zipEntry; (zipEntry = zis.getNextEntry()) != null; ) { + if (zipEntry.isDirectory()) { + zis.closeEntry(); + continue; + } + if (ENTRY_NAME_CONFIG.equals(zipEntry.getName())) { + ConnectionConfiguration configuration = loadConfiguration(zis); + // sync config name with repo location + if (repositoryLocation != null) { + String name = repositoryLocation.getName(); + ci.configuration = new ConnectionConfigurationBuilder(configuration, name).build(); + } else { + ci.configuration = configuration; + } + } else if (ENTRY_NAME_STATS.equals(zipEntry.getName())) { + ci.statistics = loadStatistics(zis); + } else if (ENTRY_NAME_ANNOTATIONS.equals(zipEntry.getName())) { + ci.annotations = Annotations.fromPropertyStyle(zis); + } else if (zipEntry.getName().endsWith(MD5_SUFFIX)) { + final String md5Hash = IOUtils.toString(zis, DEFAULT_CHARSET); + if (zipEntry.getName().startsWith(DIRECTORY_NAME_FILES)) { + final String filename = zipEntry.getName().substring(DIRECTORY_NAME_FILES.length() + 1, zipEntry.getName().length() - MD5_SUFFIX.length()); + handleOtherMD5(ci, md5hashForOtherFile, otherFileStreams, md5Hash, filename); + } else if (zipEntry.getName().startsWith(DIRECTORY_NAME_LIB)) { + final String filename = zipEntry.getName().substring(DIRECTORY_NAME_LIB.length() + 1, zipEntry.getName().length() - MD5_SUFFIX.length()); + handleLibMD5(ci, md5hashForLibFile, libFileStreams, md5Hash, filename); + } else { + LogService.getRoot().log(Level.WARNING, "Could not use entry from connection information called '" + zipEntry.getName() + "'"); + } + } else if (zipEntry.getName().startsWith(DIRECTORY_NAME_FILES)) { + final String filename = zipEntry.getName().substring(DIRECTORY_NAME_FILES.length() + 1); + handleOtherFile(ci, md5hashForOtherFile, otherFileStreams, zis, filename); + + } else if (zipEntry.getName().startsWith(DIRECTORY_NAME_LIB)) { + final String filename = zipEntry.getName().substring(DIRECTORY_NAME_LIB.length() + 1); + handleLibFile(ci, md5hashForLibFile, libFileStreams, zis, filename); + } else { + LogService.getRoot().log(Level.WARNING, "Could not use entry from connection information called '" + zipEntry.getName() + "'"); + } + zis.closeEntry(); + } + zis.close(); + zis.close2(); + + + // The following code is only used for the very unlikely case that the md5 file is missing + if (!otherFileStreams.isEmpty()) { + otherFileStreams.forEach((k, v) -> { + try { + ci.addOtherFile(k, v, null); + } catch (IOException e) { + LogService.getRoot().log(Level.SEVERE, "Could not add file to connection information", e); + } + }); + } + if (!libFileStreams.isEmpty()) { + libFileStreams.forEach((k, v) -> { + try { + ci.addLibFile(k, v, null); + } catch (IOException e) { + LogService.getRoot().log(Level.SEVERE, "Could not add lib file to connection information", e); + } + }); + } + + try { + if (repositoryLocation != null) { + ci.repository = repositoryLocation.getRepository(); + } + } catch (RepositoryException e) { + // ignore + } + return ci; + } + + /** + * Get an {@link InputStream} containing the storage structure of a {@link ConnectionInformation}. It is a zipped + * file containing a Config entry for standard configuration, a Stats entry for usage stats and two folder for Lib + * and Other files. + * + * @param connectionInformation + * the {@link ConnectionInformation} to be stored + * @param out the outputStream to write to + * @throws IOException + * in case creating the result was not possible + */ + public void serialize(ConnectionInformation connectionInformation, OutputStream out) throws IOException { + if (connectionInformation == null) { + throw new IOException("Object connection information is null"); + } + + if (out == null) { + throw new IOException("The output stream is null"); + } + + ZipOutputStream zos = new ZipOutputStream(out); + zos.setLevel(ZipOutputStream.STORED); + serializeAsZipEntry(ENTRY_NAME_CONFIG, connectionInformation.getConfiguration(), zos); + + if (connectionInformation.getStatistics() != null) { + serializeAsZipEntry(ENTRY_NAME_STATS, connectionInformation.getStatistics(), zos); + } + + if (connectionInformation.getAnnotations() != null) { + serializeAsZipEntry(ENTRY_NAME_ANNOTATIONS, connectionInformation.getAnnotations(), zos); + } + + writeAsZipEntriesWithMD5(Paths.get(DIRECTORY_NAME_LIB), connectionInformation.getLibraryFiles(), zos); + writeAsZipEntriesWithMD5(Paths.get(DIRECTORY_NAME_FILES), connectionInformation.getOtherFiles(), zos); + zos.finish(); + } + + /** + * Helper methods to create the serialized {@link ConnectionInformation}. Write an {@link Object} as JSON format to + * the {@link OutputStream}. + * + * @param out + * {@link OutputStream} to write to + * @param object + * to be written + * @throws IOException + * if writing failed + */ + public void writeJson(OutputStream out, Object object) throws IOException { + if (out != null && object != null) { + objectWriter.writeValue(out, object); + } + } + + /** + * Create {@link com.rapidminer.operator.ports.metadata.MetaData} for a {@link ConnectionInformation}. + * + * @param connectionInformation + * for which {@link com.rapidminer.operator.ports.metadata.MetaData} is required + * @return ConnectionInformationMetaData unless the connectionInformation is null, then it will be null + */ + public ConnectionInformationMetaData getMetaData(ConnectionInformation connectionInformation) { + if (connectionInformation == null) { + return null; + } + return new ConnectionInformationMetaData(connectionInformation.getConfiguration()); + } + + /** + * Load the {@link ConnectionStatistics} from an {@link InputStream}, mainly for deserializing a {@link + * ConnectionInformation} + * + * @param inputStream + * that contains the {@link ConnectionStatistics} + * @return the {@link ConnectionStatistics} that was read from the inputStream, can be null + * @throws IOException + * if reading failed + */ + ConnectionStatistics loadStatistics(InputStream inputStream) throws IOException { + return connectionStatisticsReader.readValue(inputStream); + } + + /** + * Copies the given files to the specified {@code root/dirName} and creates {@code .md5} hash files for each. + * + * @param targetFolder the target folder + * @param filesToCopy the files to write + * @param zos the ZipOutputStream to write to + * @throws IOException + */ + static void writeAsZipEntriesWithMD5(Path targetFolder, List filesToCopy, ZipOutputStream zos) throws IOException { + if (filesToCopy == null || filesToCopy.isEmpty()) { + return; + } + + if (targetFolder == null) { + targetFolder = Paths.get(""); + } + + if (zos == null) { + throw new IOException("Zip output stream cannot be null"); + } + + MessageDigest md5 = DigestUtils.getMd5Digest(); + for (Path file : filesToCopy) { + String fileName = targetFolder.resolve(file.getFileName()).toString().replace(File.separatorChar, ZIP_FILE_SEPARATOR_CHAR); + ZipEntry entry = new ZipEntry(fileName); + ZipEntry md5Entry = new ZipEntry(fileName + MD5_SUFFIX); + + // We read every file twice to speed up reading later + // - We don't have to copy every file into memory (see else case in handleOtherFile) + // - Connections should be way often read than written + + // Write Checksum first + try (InputStream is = Files.newInputStream(file); + DigestInputStream dis = new DigestInputStream(is, md5)) { + zos.putNextEntry(md5Entry); + IOUtils.skip(dis, Long.MAX_VALUE); + zos.write(Hex.encodeHexString(md5.digest()).getBytes()); + zos.closeEntry(); + } + + // Copy entry + try (InputStream is = Files.newInputStream(file)) { + zos.putNextEntry(entry); + IOUtils.copy(is, zos); + zos.closeEntry(); + } + } + } + + /** + * Writes a single zip entry + * + * @param filePath The file path + * @param object The object to serialize + * @param zos The zip outputStream + * @throws IOException + */ + private void serializeAsZipEntry(String filePath, Object object, ZipOutputStream zos) throws IOException { + zos.putNextEntry(new ZipEntry(filePath)); + objectWriter.writeValue(zos, object); + zos.closeEntry(); + } + + /** + * Handle finding a lib file's MD5 hash. See if the corresponding {@link File File's} ({@link InputStream}) was + * found or add the MD5 to the list of MD5s. + * + * @param ci + * {@link ConnectionInformation} to add the lib file to + * @param md5hashForLibFile + * map of known filenames and their MD5 hashes + * @param libFileStreams + * map of filenames to the {@link InputStream} + * @param md5Hash + * md5Hash to check, either the corresponding file was found before then it can be added, else we store the MD5 + * @param filename + * for which the MD5 is + * @throws IOException + */ + private static void handleLibMD5(ConnectionInformationImpl ci, Map md5hashForLibFile, Map libFileStreams, String md5Hash, String filename) throws IOException { + if (libFileStreams.containsKey(filename)) { + ci.addLibFile(filename, libFileStreams.get(filename), md5Hash); + libFileStreams.remove(filename); + } else { + md5hashForLibFile.put(filename, md5Hash); + } + } + + /** + * Handle 'other' file's MD5 hash. See if the corresponding {@link File File's} ({@link InputStream}) was found or + * add the MD5 to the list of MD5s. + * + * @param ci + * {@link ConnectionInformation} to add the lib file to + * @param md5hashForOtherFile + * map of known filenames and their MD5 hashes + * @param otherFileStreams + * map of filenames to the {@link InputStream} + * @param md5Hash + * md5Hash to check, either the corresponding file was found before then it can be added, else we store the MD5 + * @param filename + * for which the MD5 is + * @throws IOException + */ + private static void handleOtherMD5(ConnectionInformationImpl ci, Map md5hashForOtherFile, Map otherFileStreams, String md5Hash, String filename) throws IOException { + if (otherFileStreams.containsKey(filename)) { + ci.addOtherFile(filename, otherFileStreams.get(filename), md5Hash); + otherFileStreams.remove(filename); + } else { + md5hashForOtherFile.put(filename, md5Hash); + } + } + + /** + * Handle the lib file, add it to the {@link ConnectionInformation} if the MD5 was found or keep it to add when the + * MD5 was found + * + * @param ci + * {@link ConnectionInformation} to add the lib file to + * @param md5hashForLibFile + * map of known filenames and their MD5 hashes + * @param libFileStreams + * map of filenames to the {@link InputStream} + * @param inputStream + * the {@link InputStream} of he lib file + * @param filename + * for which the MD5 is + * @throws IOException + */ + private static void handleLibFile(ConnectionInformationImpl ci, Map md5hashForLibFile, Map libFileStreams, InputStream inputStream, String filename) throws IOException { + if (md5hashForLibFile.containsKey(filename)) { + ci.addLibFile(filename, inputStream, md5hashForLibFile.get(filename)); + md5hashForLibFile.remove(filename); + } else { + libFileStreams.put(filename, copyInputStream(inputStream)); + } + } + + /** + * Handle the file, add it to the {@link ConnectionInformation} if the MD5 was found or keep it to add when the MD5 + * was found + * + * @param ci + * {@link ConnectionInformation} to add the file to + * @param md5hashForOtherFile + * map of known filenames and their MD5 hashes + * @param otherFileStreams + * map of filenames to the {@link InputStream} + * @param inputStream + * the {@link InputStream} of he lib file + * @param filename + * for which the MD5 is + * @throws IOException + */ + private static void handleOtherFile(ConnectionInformationImpl ci, Map md5hashForOtherFile, Map otherFileStreams, InputStream inputStream, String filename) throws IOException { + if (md5hashForOtherFile.containsKey(filename)) { + ci.addOtherFile(filename, inputStream, md5hashForOtherFile.get(filename)); + md5hashForOtherFile.remove(filename); + } else { + otherFileStreams.put(filename, copyInputStream(inputStream)); + } + } + + /** + * Creates a Server Object Mapper + * + * @return an object mapper that does not encrypt values + */ + public static ObjectMapper getRemoteObjectMapper() { + ObjectMapper objectMapper = getObjectMapper(); + objectMapper.addMixIn(ValueProviderParameterImpl.class, ValueProviderParameterImpl.UnencryptedValueMixIn.class); + return objectMapper; + } + + /** + * Creates an ObjectMapper that doesn't close output streams + * + * @return a preconfigured ObjectMapper + */ + private static ObjectMapper getObjectMapper(){ + JsonFactory jsonFactory = new JsonFactory(); + // Don't close underlying streams + jsonFactory.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false); + return new ObjectMapper(jsonFactory); + } + + /** + * Copies an InputStream into memory + * + * @param inputStream + * the input stream to copy + * @return the byte array based input stream copy + * @throws NullPointerException + * if the inputStream is {@code null} + * @throws IOException + * if an I/O error occurs + */ + private static InputStream copyInputStream(InputStream inputStream) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + IOUtils.copyLarge(inputStream, baos); + return new ByteArrayInputStream(baos.toByteArray()); + } + +} diff --git a/src/main/java/com/rapidminer/connection/ConnectionStatistics.java b/src/main/java/com/rapidminer/connection/ConnectionStatistics.java new file mode 100644 index 000000000..bd57744d3 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/ConnectionStatistics.java @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection; + +import java.time.ZonedDateTime; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** + * An interface for simple statistics about a connection. This keeps track of the last change to the connection, + * the last successful test as well as the last test error. The statistics can be updated, internally using {@link ZonedDateTime#now()} + * for the dates. + * + * @author Jan Czogalla + * @since 9.3 + */ +@JsonDeserialize(as = ConnectionStatisticsImpl.class) +public interface ConnectionStatistics { + + /** Gets the time of last successful test */ + ZonedDateTime getLastSuccess(); + + /** Gets the time of last recorded change */ + ZonedDateTime getLastChange(); + + /** Gets the error of last failed test */ + String getLastError(); + + /** Updates the time of last successful test with now */ + ConnectionStatistics updateSuccess(); + + /** Updates the time of last change with now */ + ConnectionStatistics updateChange(); + + /** + * Updates the error of last failed test + * + * @param error + * the error message + */ + ConnectionStatistics updateError(String error); + +} diff --git a/src/main/java/com/rapidminer/connection/ConnectionStatisticsBuilder.java b/src/main/java/com/rapidminer/connection/ConnectionStatisticsBuilder.java new file mode 100644 index 000000000..b293047a2 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/ConnectionStatisticsBuilder.java @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection; + +import java.io.IOException; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.rapidminer.tools.ValidationUtil; + +/** + * Builder for {@link ConnectionStatistics}. Can create a new instance based on an existing {@link ConnectionStatistics} + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +class ConnectionStatisticsBuilder { + + private ConnectionStatistics object; + + private static final ObjectWriter writer; + private static final ObjectReader reader; + + static { + ObjectMapper mapper = new ObjectMapper(); + reader = mapper.reader(ConnectionStatisticsImpl.class); + writer = mapper.writerWithType(ConnectionStatisticsImpl.class); + } + + + /** + * Create a builder based on an existing {@link ConnectionStatistics} + */ + ConnectionStatisticsBuilder(ConnectionStatistics original) throws IOException { + ValidationUtil.requireNonNull(original, "original connection statistics"); + object = reader.readValue(writer.writeValueAsBytes(original)); + } + + /** + * Build and return the new {@link ConnectionStatistics}. Afterwards, the builder becomes invalid. + */ + public ConnectionStatistics build() { + ConnectionStatistics statistics = object; + object = null; + return statistics; + } +} diff --git a/src/main/java/com/rapidminer/connection/ConnectionStatisticsImpl.java b/src/main/java/com/rapidminer/connection/ConnectionStatisticsImpl.java new file mode 100644 index 000000000..e09ad9776 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/ConnectionStatisticsImpl.java @@ -0,0 +1,144 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection; + +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.apache.commons.lang.StringUtils; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; + + +/** + * Implementation of {@link ConnectionStatistics} + * + * @author Jan Czogalla + * @since 9.3 + */ +@JsonAutoDetect(getterVisibility = Visibility.NONE) +public class ConnectionStatisticsImpl implements ConnectionStatistics { + + private ZonedDateTime lastSuccess; + private ZonedDateTime lastChange; + private String lastError; + + /** Minimal constructor for Json; no change, no statistics */ + ConnectionStatisticsImpl() { + lastChange = ZonedDateTime.now(); + } + + /** Constructor with successful start. Also updates time of change with the same time as the success */ + public ConnectionStatisticsImpl(ZonedDateTime lastSuccess) { + this.lastSuccess = lastSuccess; + this.lastChange = lastSuccess; + } + + /** Constructor with failed start. Also updates time of change with now */ + public ConnectionStatisticsImpl(String lastError) { + this(); + this.lastError = lastError; + } + + @Override + public ZonedDateTime getLastSuccess() { + return lastSuccess; + } + + @Override + public ZonedDateTime getLastChange() { + return lastChange; + } + + @Override + public String getLastError() { + return lastError; + } + + @Override + public ConnectionStatistics updateSuccess() { + this.lastSuccess = ZonedDateTime.now(); + return this; + } + + @Override + public ConnectionStatistics updateChange() { + // lets start and keep track of all changes here if possible (enterprise feature audit) // comment by AnTi + this.lastChange = ZonedDateTime.now(); + return this; + } + + @Override + public ConnectionStatistics updateError(String error) { + this.lastError = error; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConnectionStatisticsImpl that = (ConnectionStatisticsImpl) o; + return Objects.equals(lastSuccess, that.lastSuccess) && + Objects.equals(lastChange, that.lastChange) && + Objects.equals(lastError, that.lastError); + } + + @Override + public int hashCode() { + return Objects.hash(lastSuccess, lastChange, lastError); + } + + /** Json specific getter to convert dates to strings */ + @JsonAnyGetter + private Map getJsonValues() { + Map values = new HashMap<>(); + values.put("lastSuccess", lastSuccess != null ? lastSuccess.toString() : null); + values.put("lastChange", lastChange != null ? lastChange.toString() : null); + values.put("lastError", lastError); + return values; + } + + /** Json specific setter to convert strings to dates */ + @JsonAnySetter + private void setJsonValue(String name, String value) { + switch (name) { + case "lastChange": + this.lastChange = ZonedDateTime.parse(Objects.requireNonNull(StringUtils.trimToNull(value))); + return; + case "lastSuccess": + this.lastSuccess = StringUtils.trimToNull(value) == null ? null : ZonedDateTime.parse(value); + return; + case "lastError": + this.lastError = StringUtils.trimToNull(value); + return; + default: + } + } + +} diff --git a/src/main/java/com/rapidminer/connection/CreateI18NKeysForConnectionHandler.java b/src/main/java/com/rapidminer/connection/CreateI18NKeysForConnectionHandler.java new file mode 100644 index 000000000..7229df9ed --- /dev/null +++ b/src/main/java/com/rapidminer/connection/CreateI18NKeysForConnectionHandler.java @@ -0,0 +1,504 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection; + +import static com.rapidminer.connection.util.ConnectionI18N.GROUP_PREFIX; +import static com.rapidminer.connection.util.ConnectionI18N.ICON_SUFFIX; +import static com.rapidminer.connection.util.ConnectionI18N.KEY_DELIMITER; +import static com.rapidminer.connection.util.ConnectionI18N.LABEL_SUFFIX; +import static com.rapidminer.connection.util.ConnectionI18N.PARAMETER_PREFIX; +import static com.rapidminer.connection.util.ConnectionI18N.TIP_SUFFIX; +import static com.rapidminer.connection.util.ConnectionI18N.TYPE_PREFIX; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.function.BiFunction; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.WordUtils; + +import com.rapidminer.connection.adapter.ConnectionAdapter; +import com.rapidminer.connection.adapter.ConnectionAdapterHandler; +import com.rapidminer.connection.configuration.ConnectionConfiguration; +import com.rapidminer.parameter.ParameterHandler; +import com.rapidminer.parameter.ParameterType; + + +/** + * Helper class to prepare the {@code GUI{Extension}.properties} file. Provides the following functionality: + *

    + *
  • {@link #appendOrReplaceConnectionKeys(Runnable, String, Path, BiFunction) appendOrReplaceConnectionKeys}: + * Main method to add missing keys regarding connections and prefilling them where possible
  • + *
  • {@link #getDefaultGUIPropertyPath(String)}: Method to get the default path of the + * {@code GUI{Extension}.properties} file; resolves to + * {@code {project}/src/main/resources/com/rapidminer/resources/i18n/GUI{infix}.properties} + * and makes it absolute
  • + *
  • {@link #connectionAdaptionHandlerDefaultValues(String)}: Method to get the default values for a + * {@link ConnectionAdapterHandler}; this will probably resolve most of the new keys already
  • + *
+ * + * @author Jan Czogalla + * @see com.rapidminer.connection.util.ConnectionI18N ConnectionI18N + * @since 9.3 + */ +public class CreateI18NKeysForConnectionHandler { + + private static final String HEADER_LINE = "####################"; + private static final String CONNECTION_HEADER = "## Connections #"; + + private CreateI18NKeysForConnectionHandler() {} + + /** + * Uses the given {@code propertyPath} as a basis to fill in the new connection related keys. + * The method works as follows: + *
    + *
  1. If the {@code initializer} is not {@code null}, run it
  2. + *
  3. If the {@code defaultValues} is {@code null}, use a {@link BiFunction} that returns an empty string
  4. + *
  5. Find all registered {@link ConnectionHandler ConnectionHandlers} that have {@code namespace} as a prefix; + * if none are found, return {@code null}
  6. + *
  7. For each handler, create all relevant i18n keys for the type, all parameter groups, and each parameter; + * they will be sorted by group name and the parameters will also be sorted alphabetically inside each group
  8. + *
  9. Load the existing properties from {@code propertyPath} and use them together with {@code defaultValues} + * to initialize all keys where possible
  10. + *
  11. Create a backup of {@code propertyPath} and {@link #writeProperties(String, Map, Path, Path) write} + * all keys to a new path that will then be returned
  12. + *
+ * + * @param initializer + * an initializer if necessary; can be {@code null}; can be used to register all relevant + * {@link ConnectionHandler ConnectionHandlers} + * @param namespace + * the namespace of the relevant {@link ConnectionHandler ConnectionHandlers}; must not be {@code null} or empty + * @param propertyPath + * the path to the file to take as a basis; must not be {@code null} and must exist + * @param defaultValues + * a {@link BiFunction} that provides a default value; can be {@code null} + * @return the path of the new file with the combined keys from the original file and the (sorted) connection keys + * or {@code null} if no relevant handlers were found + * @throws IOException + * if an I/O error occurs + */ + public static Path appendOrReplaceConnectionKeys(Runnable initializer, String namespace, Path propertyPath, + BiFunction defaultValues) throws IOException { + if (initializer != null) { + initializer.run(); + } + + BiFunction defaultValueProvider; + if (defaultValues == null) { + defaultValueProvider = (key, props) -> ""; + } else { + defaultValueProvider = defaultValues; + } + + ConnectionHandlerRegistry handlerRegistry = ConnectionHandlerRegistry.getInstance(); + List connections = handlerRegistry.getAllTypes().stream() + .filter(type -> type.startsWith(namespace + ':')) + .map(handlerRegistry::getHandler).map(h -> h.createNewConnectionInformation("test")) + .collect(Collectors.toList()); + if (connections.isEmpty()) { + return null; + } + + List typeSuffixes = Arrays.asList(LABEL_SUFFIX, TIP_SUFFIX, ICON_SUFFIX); + List groupSuffixes = Arrays.asList(LABEL_SUFFIX, TIP_SUFFIX, ICON_SUFFIX); + List parameterSuffixes = Arrays.asList(LABEL_SUFFIX, TIP_SUFFIX); + SortedMap>> connectionI18NProperties = new TreeMap<>(); + + // Create all i18n keys for type, groups and parameters + for (ConnectionInformation connection : connections) { + ConnectionConfiguration configuration = connection.getConfiguration(); + String type = configuration.getType(); + TreeMap> typeKeys = new TreeMap<>(); + // store type keys as first group + typeKeys.put("", typeSuffixes.stream().map(suffix -> getKeyForType(type, suffix)) + .collect(Collectors.toCollection(TreeSet::new))); + + // collect group keys and parameter keys per group + configuration.getKeys().forEach(group -> { + SortedSet groupKeys = new TreeSet<>(); + String groupKey = group.getGroup(); + groupSuffixes.stream().map(suffix -> getKeyForGroup(type, groupKey, suffix)).forEach(groupKeys::add); + parameterSuffixes.stream().flatMap(suffix -> group.getParameters().stream() + .map(cp -> getKeyForParameter(type, groupKey, cp.getName(), suffix))).forEach(groupKeys::add); + if (!groupKey.isEmpty()) { + typeKeys.put(groupKey, groupKeys); + } + }); + if (!typeKeys.isEmpty()) { + connectionI18NProperties.put(type, typeKeys); + } + } + + Properties properties = new Properties(); + try (BufferedReader reader = Files.newBufferedReader(propertyPath)) { + properties.load(reader); + } + + // find default values from properties and the given bifunction + Map>> connectionPropertyValues = new LinkedHashMap<>(); + connectionI18NProperties.forEach((type, typeKeys) -> typeKeys.forEach((group, groupKeys) -> { + String typeKey = getKeyForType(type, LABEL_SUFFIX); + String typeName = properties.getProperty(typeKey, + defaultValueProvider.andThen(n -> n.isEmpty() ? typeKey : n).apply(typeKey, properties)); + List groupKeyValues = groupKeys.stream() + .map(key -> key + " = " + properties.getProperty(key, defaultValueProvider.apply(key, properties))) + .collect(Collectors.toList()); + connectionPropertyValues.computeIfAbsent(typeName, x -> new ArrayList<>()).add(groupKeyValues); + })); + + Path backupPath = Paths.get(propertyPath.toString() + ".bak"); + Files.copy(propertyPath, backupPath, REPLACE_EXISTING); + Path newPath = Paths.get(propertyPath.toString() + ".new"); + + writeProperties(namespace, connectionPropertyValues, backupPath, newPath); + return newPath; + } + + /** + * Creates and returns an absolute {@link Path} that resolves to + * {@code {projectPath}/src/main/resources/com/rapidminer/resources/i18n/GUI{infix}.properties} + * + * @param infix + * the infix for the property file name; usually the camel cased namespace of an extension + * @return the absolute path + */ + public static Path getDefaultGUIPropertyPath(String infix) { + String propertyFileName = "src/main/resources/com/rapidminer/resources/i18n/GUI" + infix + ".properties"; + return Paths.get(propertyFileName).toAbsolutePath(); + } + + /** + * Returns a {@link BiFunction} that can extract default values for {@link ConnectionAdapterHandler} based + * connection keys. Will resolve i18n keys as follows; unresolved keys will result in an empty string return value: + *
    + *
  • Icons and tips for groups will not be resolved and just return the empty string
  • + *
  • Labels for groups and parameters will be capitalized and spaced, + * i.e. {@code parameter_key} will be transformed to "Parameter key"
  • + *
  • Tips for parameters will be resolved through {@link ParameterType#getDescription()} + * from the appropriate parameter type obtained from + * {@link ConnectionAdapterHandler#getParameterTypes(ParameterHandler)}; it then is put through the original + * properties to make sure that if description was based on i18n before, it will be resolved properly
  • + *
  • Keys for types will be resolved by looking up the values of + * {@link ConnectionAdapterHandler#getIconName()}, {@link ConnectionAdapterHandler#getName()} and + * {@link ConnectionAdapterHandler#getDescription()} in the original properties for the + * icon, label and tip respectively; if the name/label contains a "Connection" suffix, that will be removed
  • + *
+ * Note: This method might be removed in the future. + * + * @param namespace + * the namespace prefix of all affected handlers + * @return the default value function + */ + public static BiFunction connectionAdaptionHandlerDefaultValues(String namespace) { + return (i18nKey, properties) -> connectionAdaptionHandlerDefaultValues(namespace, i18nKey, properties); + } + + /** + * Creates the i18n key for the given connection type and suffix + * + * @param type + * the type of the connection whose key should be created + * @param suffix + * the suffix; one of "icon", "label" or "tip" + * @return the i18n key + */ + private static String getKeyForType(String type, String suffix) { + return getKeyFor(type, TYPE_PREFIX, suffix); + } + + /** + * Creates the i18n key for the given connection type, group and suffix + * + * @param type + * the type of the connection + * @param group + * the group whose key should be created + * @param suffix + * the suffix; one of "icon", "label" or "tip" + * @return the i18n key + */ + private static String getKeyForGroup(String type, String group, String suffix) { + return getKeyFor(type, GROUP_PREFIX, group, suffix); + } + + /** + * Creates the i18n key for the given connection type, group, parameter and suffix + * + * @param type + * the type of the connection + * @param group + * the group of the parameter + * @param parameter + * the parameter whose key should be created + * @param suffix + * the suffix; one of "label" or "tip" + * @return the i18n key + */ + private static String getKeyForParameter(String type, String group, String parameter, String suffix) { + return getKeyFor(type, PARAMETER_PREFIX, group, parameter, suffix); + } + + /** + * Creates the i18n key for the given connection type and other pieces. The first piece is the actual needed prefix + * (one of {@value com.rapidminer.connection.util.ConnectionI18N#TYPE_PREFIX}, + * {@value com.rapidminer.connection.util.ConnectionI18N#GROUP_PREFIX} or + * {@value com.rapidminer.connection.util.ConnectionI18N#PARAMETER_PREFIX}), the other pieces describe the infix + * (group/parameter and actual name/key) as well as the suffix. + * + * @param type + * the type of the connection + * @param pieces + * the different needed pieces + * @return the i18n key + */ + private static String getKeyFor(String type, String... pieces) { + type = type.replace(':', '.'); + return String.join(KEY_DELIMITER, (String[]) ArrayUtils.add(pieces, 1, type)); + } + + /** + * Writes all properties to the given {@code newPath} after reading in {@code backupPath}. Adds all connection + * related keys in one block which might be appended to the end of the file if it did not exist before. Will keep + * an existing block in the same space and expand on it. Also will remove all keys that are outside of that block. + * + * @param namespace + * the namespace of the affected handlers + * @param connectionPropertyValues + * the key/value pair lines, in blocks and sorted + * @param backupPath + * the original file to expand on + * @param newPath + * the new file to write to + * @throws IOException + * if an I/O error occurs + * @see #writeProperties(BufferedWriter, Map) + */ + private static void writeProperties(String namespace, Map>> connectionPropertyValues, + Path backupPath, Path newPath) throws IOException { + String[] prefixes = Stream.of(TYPE_PREFIX, GROUP_PREFIX, PARAMETER_PREFIX) + .map(p -> p + '.' + namespace).toArray(String[]::new); + try (BufferedReader reader = Files.newBufferedReader(backupPath); + BufferedWriter writer = Files.newBufferedWriter(newPath, CREATE, TRUNCATE_EXISTING)) { + int status = 0; + String line; + while ((line = reader.readLine()) != null) { + switch (status) { + case 0: + if (StringUtils.startsWithAny(line, prefixes)) { + // skip connection keys that are not collected in the proper area + // they will be added again later + break; + } + writer.write(line); + writer.newLine(); + if (line.equals(CONNECTION_HEADER)) { + status = 1; + } + break; + case 1: + writer.write(line); + writer.newLine(); + writeProperties(writer, connectionPropertyValues); + while ((line = reader.readLine()) != null && !line.startsWith("##")) { + //skip existing connection entries + } + if (line != null) { + writer.newLine(); + writer.write(line); + writer.newLine(); + } + status = 2; + break; + case 2: + writer.write(line); + writer.newLine(); + break; + default: + break; + } + } + if (status == 0) { + // no connections so far, append it all + writer.newLine(); + writer.write(HEADER_LINE); + writer.newLine(); + writer.write(CONNECTION_HEADER); + writer.newLine(); + writer.write(HEADER_LINE); + writer.newLine(); + writeProperties(writer, connectionPropertyValues); + } + } + } + + /** + * Writes the actual connection i18n keys to the file as one block. Each type has a leading comment in the form of + * {@code # {type name or type key}}, followed by blocks of the type and its groups. + * + * @param writer + * the file writer + * @param connectionPropertyValues + * the i18n keys + * @throws IOException + * if an I/O error occurs + */ + private static void writeProperties(BufferedWriter writer, Map>> connectionPropertyValues) throws IOException { + for (Entry>> entry : connectionPropertyValues.entrySet()) { + writer.newLine(); + writer.write("# " + entry.getKey()); + for (List groups : entry.getValue()) { + writer.newLine(); + for (String group : groups) { + writer.write(group); + writer.newLine(); + } + } + } + } + + /** + * Returns a {@link BiFunction} that can extract default values for {@link ConnectionAdapterHandler} based + * connection keys. Will resolve i18n keys as follows; unresolved keys will result in an empty string return value: + *
    + *
  • Icons and tips for groups will not be resolved and just return the empty string
  • + *
  • Labels for groups and parameters will be capitalized and spaced, + * i.e. {@code parameter_key} will be transformed to "Parameter key"
  • + *
  • Tips for parameters will be resolved through {@link ParameterType#getDescription()} + * from the appropriate parameter type obtained from + * {@link ConnectionAdapterHandler#getParameterTypes(ParameterHandler)}; it then is put through the original + * properties to make sure that if description was based on i18n before, it will be resolved properly
  • + *
  • Keys for types will be resolved by looking up the values of + * {@link ConnectionAdapterHandler#getIconName()}, {@link ConnectionAdapterHandler#getName()} and + * {@link ConnectionAdapterHandler#getDescription()} in the original properties for the + * icon, label and tip respectively; if the name/label contains a "Connection" suffix, that will be removed
  • + *
+ * + * @param namespace + * the namespace prefix of all affected handlers + * @param i18nKey + * the i18n key to be resolved + * @param properties + * the original properties + * @return the default value + */ + private static String connectionAdaptionHandlerDefaultValues(String namespace, String i18nKey, Properties properties) { + if (i18nKey.startsWith(GROUP_PREFIX)) { + if (i18nKey.endsWith(LABEL_SUFFIX)) { + return capitalizeName(i18nKey); + } + return ""; + } + if (i18nKey.startsWith(PARAMETER_PREFIX)) { + if (i18nKey.endsWith(LABEL_SUFFIX)) { + return capitalizeName(i18nKey); + } + if (i18nKey.endsWith(TIP_SUFFIX)) { + String type = extractType(i18nKey, namespace); + ConnectionAdapterHandler handler = ConnectionAdapterHandler.getHandler(type); + if (handler == null) { + return ""; + } + String paramKey = extractName(i18nKey); + return handler.getParameterTypes(null).stream() + .filter(pt -> pt.getKey().equals(paramKey)).findFirst() + .map(ParameterType::getDescription).map(desc -> properties.getProperty(desc, desc)) + .orElse(""); + } + } + if (i18nKey.startsWith(TYPE_PREFIX)) { + ConnectionAdapterHandler handler = ConnectionAdapterHandler.getHandler(extractType(i18nKey, namespace)); + if (handler == null) { + return ""; + } + if (i18nKey.endsWith(LABEL_SUFFIX)) { + return properties.getProperty(handler.getName(), "").replace(" Connection", ""); + } else if (i18nKey.endsWith(ICON_SUFFIX)) { + return properties.getProperty(handler.getIconName(), ""); + } else if (i18nKey.endsWith(TIP_SUFFIX)) { + return properties.getProperty(handler.getDescription(), ""); + } + } + return ""; + } + + /** + * Turns a parameter key into a capitalized version to be used as a name, + * e.g. {@code parameter_key} is turned into "Parameter key" + * + * @param i18nKey + * the i18n key of the parameter + * @return the capitalized name + */ + private static String capitalizeName(String i18nKey) { + return WordUtils.capitalize(extractName(i18nKey).replace('_', ' ')); + } + + /** + * Extracts the name part from the given i18n key; this is defined as the second to last segment + * when splitting by '.'. + * + * @param i18nKey + * the i18n key + * @return the actual name part + */ + private static String extractName(String i18nKey) { + int end = i18nKey.lastIndexOf('.'); + int start = i18nKey.lastIndexOf('.', end - 1); + return i18nKey.substring(start + 1, end); + } + + /** + * Extracts the type part from the given i18n key; this is defined as the segment after {@code namespace} + * when splitting by '.'. + * + * @param i18nKey + * the i18n key + * @return the actual type part + */ + private static String extractType(String i18nKey, String namespace) { + int namespacePos = i18nKey.indexOf(namespace); + if (namespacePos == -1) { + return ""; + } + int start = namespacePos + namespace.length() + 1; + return i18nKey.substring(start, i18nKey.indexOf('.', start)); + } +} diff --git a/src/main/java/com/rapidminer/connection/DefaultValueProviderGUI.java b/src/main/java/com/rapidminer/connection/DefaultValueProviderGUI.java new file mode 100644 index 000000000..8565d1fe2 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/DefaultValueProviderGUI.java @@ -0,0 +1,123 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection; + +import java.awt.GridLayout; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JPasswordField; +import javax.swing.JTextField; + +import com.rapidminer.connection.gui.AbstractConnectionGUI; +import com.rapidminer.connection.gui.ValueProviderGUIProvider; +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.connection.valueprovider.ValueProvider; +import com.rapidminer.connection.valueprovider.ValueProviderParameter; +import com.rapidminer.tools.I18N; + + +/** + * The default GUI provider to configure the parameters of a {@link ValueProvider} + * + * @author Jonas Wilms-Pfau, Andreas Timm + * @since 9.3 + */ +public class DefaultValueProviderGUI implements ValueProviderGUIProvider { + + /** + * Generic text which states that no config is needed + */ + protected static final String NO_CONFIG_NEEDED_LABEL = I18N.getGUIMessage("gui.dialog.connection.valueprovider.needs_no_configuration.label"); + /** + * Some stars for encrypted values + */ + private static final String ENCRYPTED_PLACEHOLDER = ConnectionI18N.getConnectionGUILabel("placeholder_encrypted"); + + @Override + public JComponent createConfigurationComponent(JDialog parent, ValueProvider provider, ConnectionInformation connection, boolean editmode) { + if (provider.getParameters().isEmpty()) { + JLabel noConfigLabel = new JLabel(NO_CONFIG_NEEDED_LABEL); + return addInformationIcon(noConfigLabel, provider.getType(), "no_configuration", parent); + } + + final JPanel panel = new JPanel(new GridLayout(0, 2, 50, 8)); + + for (ValueProviderParameter parameter : provider.getParameters()) { + panel.add(new JLabel(ConnectionI18N.getParameterName(provider.getType(), "valueprovider", + parameter.getName(), parameter.getName()))); + panel.add(addInformationIcon(getEditComponent(parameter, editmode), provider.getType(), parameter.getName(), parent)); + } + + return panel; + } + + /** + * Creates an edit component for a value provider parameter + * + * @param parameter + * the parameter + * @param editmode + * if the parameter is editable + * @return the component for the parameter + */ + protected static JComponent getEditComponent(ValueProviderParameter parameter, boolean editmode) { + final JComponent comp; + if (editmode) { + final JTextField textField = parameter.isEncrypted() ? new JPasswordField() : new JTextField(); + textField.setText(parameter.getValue()); + textField.addKeyListener(new KeyAdapter() { + @Override + public void keyReleased(KeyEvent e) { + parameter.setValue(textField.getText()); + } + }); + textField.setColumns(20); + comp = textField; + } else { + comp = new JLabel(parameter.isEncrypted() ? ENCRYPTED_PLACEHOLDER : parameter.getValue()); + } + comp.setEnabled(parameter.isEnabled()); + return comp; + } + + /** + * Wraps the given input component in a panel and adds an information icon with a tooltip. The tooltip i18n is + * derived from the type, group and parameter name as {@code gui.label.connection.parameter.{type}.valueprovider.{ + * parameterName}.tip} + * + * @param parameterInputComponent + * the component to wrap + * @param type + * the type of the value provider + * @param parameterName + * the name of the value provider parameter + * @param parent + * the parent dialog + * @return a new panel containing the old and an additional information icon with tooltip + */ + protected static JPanel addInformationIcon(JComponent parameterInputComponent, String type, + String parameterName, JDialog parent) { + return AbstractConnectionGUI.addInformationIcon(parameterInputComponent, ConnectionI18N.getParameterTooltip(type, + "valueprovider", parameterName, null), parent); + } +} diff --git a/src/main/java/com/rapidminer/connection/adapter/ConnectionAdapter.java b/src/main/java/com/rapidminer/connection/adapter/ConnectionAdapter.java new file mode 100644 index 000000000..4ce6133fb --- /dev/null +++ b/src/main/java/com/rapidminer/connection/adapter/ConnectionAdapter.java @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.adapter; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.legacy.ConversionException; +import com.rapidminer.tools.config.AbstractConfigurable; + +/** + * An adapter helper class for {@link com.rapidminer.tools.config.Configurable Configurables} and + * {@link ConnectionInformation ConnectionInformations}. Indicates that this configurable supports the + * connection information mechanism and provides a + * {@link ConnectionAdapterHandler#convert(ConnectionAdapter) conversion method}. + *

+ * Note: Should always be used in conjunction with a {@link ConnectionAdapterHandler} + * + * @author Jan Czogalla, Gisa Meier + * @see ConnectionAdapterHandler + * @since 9.3 + */ +public abstract class ConnectionAdapter extends AbstractConfigurable { + + /** @return always {@code true} */ + @Override + public final boolean supportsNewConnectionManagement() { + return true; + } + + /** @see ConnectionAdapterHandler#convert(ConnectionAdapter) */ + @Override + public final ConnectionInformation convert() throws ConversionException { + ConnectionAdapterHandler handler = ConnectionAdapterHandler.getHandler(getTypeId()); + if (handler == null) { + throw new ConversionException("No handler for " + getTypeId()); + } + return handler.convert(this); + } +} diff --git a/src/main/java/com/rapidminer/connection/adapter/ConnectionAdapterException.java b/src/main/java/com/rapidminer/connection/adapter/ConnectionAdapterException.java new file mode 100644 index 000000000..f75357b9d --- /dev/null +++ b/src/main/java/com/rapidminer/connection/adapter/ConnectionAdapterException.java @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.adapter; + +import com.rapidminer.connection.configuration.ConnectionConfiguration; +import com.rapidminer.connection.util.ValidationResult; +import com.rapidminer.operator.Operator; +import com.rapidminer.operator.UserError; + +/** + * A special {@link UserError} that contains a {@link ValidationResult}. Used when testing and retrieving + * {@link ConnectionAdapter ConnectionAdapters}. + * + * @author Jan Czogalla + * @since 9.3 + */ +public class ConnectionAdapterException extends UserError { + + private final ValidationResult validation; + + /** + * Creates a new {@link ConnectionAdapterException} associated with an {@link Operator}, + * a {@link ConnectionConfiguration} and a {@link ValidationResult}. + * + * @param operator + * the operator that the error occurred in; might be {@code null} + * @param configuration + * the configuration that the error occurred for; must not be {@code null} + * @param validation + * the validation result associated with the error; might be {@code null} + */ + public ConnectionAdapterException(Operator operator, ConnectionConfiguration configuration, ValidationResult validation) { + super(operator, "connection.adapter.validation_error", configuration.getName(), configuration.getType()); + this.validation = validation; + } + + /** @return the associated {@link ValidationResult}; might be {@code null} */ + public ValidationResult getValidation() { + return validation; + } +} diff --git a/src/main/java/com/rapidminer/connection/adapter/ConnectionAdapterHandler.java b/src/main/java/com/rapidminer/connection/adapter/ConnectionAdapterHandler.java new file mode 100644 index 000000000..b76487543 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/adapter/ConnectionAdapterHandler.java @@ -0,0 +1,769 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.adapter; + +import static com.rapidminer.connection.util.ConnectionInformationSelector.createParameterTypes; +import static com.rapidminer.tools.FunctionWithThrowable.suppress; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.stream.Collectors; + +import com.rapidminer.Process; +import com.rapidminer.connection.ConnectionHandler; +import com.rapidminer.connection.ConnectionHandlerRegistry; +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.ConnectionInformationBuilder; +import com.rapidminer.connection.configuration.ConfigurationParameter; +import com.rapidminer.connection.configuration.ConfigurationParameterImpl; +import com.rapidminer.connection.configuration.ConnectionConfiguration; +import com.rapidminer.connection.configuration.ConnectionConfigurationBuilder; +import com.rapidminer.connection.legacy.ConversionException; +import com.rapidminer.connection.util.ConnectionInformationSelector; +import com.rapidminer.connection.util.ConnectionSelectionProvider; +import com.rapidminer.connection.util.GenericHandler; +import com.rapidminer.connection.util.TestExecutionContext; +import com.rapidminer.connection.util.TestResult; +import com.rapidminer.connection.util.TestResult.ResultType; +import com.rapidminer.connection.util.ValidationResult; +import com.rapidminer.connection.valueprovider.handler.ValueProviderHandlerRegistry; +import com.rapidminer.operator.Operator; +import com.rapidminer.operator.ProcessSetupError; +import com.rapidminer.operator.ProcessSetupError.Severity; +import com.rapidminer.operator.SimpleProcessSetupError; +import com.rapidminer.operator.UserError; +import com.rapidminer.operator.ports.InputPort; +import com.rapidminer.operator.ports.metadata.MDTransformationRule; +import com.rapidminer.operator.ports.metadata.MetaDataError; +import com.rapidminer.parameter.ParameterHandler; +import com.rapidminer.parameter.ParameterType; +import com.rapidminer.parameter.ParameterTypePassword; +import com.rapidminer.parameter.ParameterTypeStringCategory; +import com.rapidminer.parameter.Parameters; +import com.rapidminer.parameter.SimpleListBasedParameterHandler; +import com.rapidminer.parameter.conditions.EqualStringCondition; +import com.rapidminer.parameter.conditions.NonEqualStringCondition; +import com.rapidminer.parameter.conditions.PortConnectedCondition; +import com.rapidminer.repository.RepositoryAccessor; +import com.rapidminer.tools.I18N; +import com.rapidminer.tools.LogService; +import com.rapidminer.tools.ValidationUtil; +import com.rapidminer.tools.config.AbstractConfigurator; +import com.rapidminer.tools.config.Configurable; +import com.rapidminer.tools.config.ConfigurableConnectionHandler; +import com.rapidminer.tools.config.ConfigurationException; +import com.rapidminer.tools.config.ConfigurationManager; +import com.rapidminer.tools.config.ParameterTypeConfigurable; +import com.rapidminer.tools.config.TestConfigurableAction; +import com.rapidminer.tools.config.actions.ActionResult; +import com.rapidminer.tools.usagestats.ActionStatisticsCollector; + + +/** + * {@link ConnectionHandler} which can convert an existing {@link ConnectionAdapter} into a + * {@link ConnectionInformation} and back. + *

+ * This class also provides helper methods to centralize the integration with {@link ConnectionInformationSelector}, + * {@link ParameterType ParameterTypes} and {@link ConnectionAdapter ConnectionAdapters}. + *

    + *
  • {@link #registerHandler(ConnectionAdapterHandler) Register a handler for an adapter creator}
  • + *
  • {@link #getHandler(String) Get the appropriate handler for a given type}
  • + *
  • {@link #getConnectionParameters(Operator, String, ParameterTypeConfigurable) + * Get connection/configurable related parameters and register a selector if possible}
  • + *
  • {@link #getAdapter(Operator, String, String) Get the adapter specified in an operator}
  • + *
+ *

+ * Subclasses should check if they need to override specific methods + *

    + *
  • {@link #parametersByGroup()}: Specify which parameters will go in which group; + * by default, all parameters specified by {@link #getParameterTypes(ParameterHandler)} will be grouped + * in the {@value #BASE_GROUP} group
  • + *
  • {@link #getAdditionalActions()}: Additional actions such as clearing the cache or configuring + * an environment should be returned here. By default, this is an empty map
  • + *
  • {@link #initialize()}: If additional setup is necessary for the handler, + * do it in this method. If you do override this method, you also need to override {@link #isInitialized()}. + * By default the handler is initialized with a noop.
  • + *
  • {@link #validate(ConnectionInformation)} and {@link #test(TestExecutionContext)}: + * If you need to do specialty checks, you can override the validation and testing methods. By default, they rely on + * the relation between the parameters provided by {@link #getParameterTypes(ParameterHandler)} and + * {@link ConnectionAdapter#getTestAction()} respectively.
  • + *
+ * + * @author Jan Czogalla, Gisa Meier + * @see AbstractConfigurator + * @see + * Confluence article for converting configurables to new connection management + * @since 9.3 + */ +public abstract class ConnectionAdapterHandler + extends AbstractConfigurator implements ConfigurableConnectionHandler { + + /** The group key for the default parameter group */ + public static final String BASE_GROUP = "basic"; + + //region parameter related constants + /** Operator parameter key for the selection between old configurables and new connection locations */ + public static final String PARAMETER_CONNECTION_SOURCE = "connection_source"; + + /** Indicator to use old configurables for {@link #PARAMETER_CONNECTION_SOURCE} */ + public static final String PREDEFINED_MODE = "predefined"; + /** Indicator to use new connection location for {@link #PARAMETER_CONNECTION_SOURCE} */ + public static final String REPOSITORY_MODE = "repository"; + /** Dropdown list for {@link #PARAMETER_CONNECTION_SOURCE} */ + private static final String[] CONNECTION_SOURCE_MODES = {PREDEFINED_MODE, REPOSITORY_MODE}; + //endregion + + /** Map of all registered {@link ConnectionHandler ConnectionHandlers} that are based on {@link ConnectionAdapterHandler} */ + private static final Map handlerMap = new HashMap<>(); + + /** The namespace of this handler, might be {@code null} */ + private final String namespace; + + /** + * Constructor for subclasses; the namespace is optional, but if provided, must not be empty + * + * @param namespace {@code null} or a non-empty string + */ + protected ConnectionAdapterHandler(String namespace) { + if (namespace != null) { + this.namespace = ValidationUtil.requireNonEmptyString(namespace, "namespace"); + } else { + this.namespace = null; + } + } + + //region implementation of ConnectionHandler + @Override + public boolean isInitialized() { + return true; + } + + @Override + public String getType() { + if (namespace == null) { + return getTypeId(); + } + return namespace + ':' + getTypeId(); + } + + /** + * {@inheritDoc} + *

+ * This handler uses {@link #create(String, Map)} to create a new {@link ConnectionAdapter} and then converts it. + * + * @return a new connection or {@code null} if an error occurs + * @see #convert(ConnectionAdapter) + */ + @Override + public ConnectionInformation createNewConnectionInformation(String name) { + try { + Map defaultValues = getParameterTypes(null).stream() + .filter(p -> p.getDefaultValueAsString() != null && !p.getDefaultValueAsString().isEmpty()) + .collect(Collectors.toMap(ParameterType::getKey, ParameterType::getDefaultValueAsString)); + return convert(create(name, defaultValues)); + } catch (ConfigurationException | ConversionException e) { + LogService.getRoot().log(Level.WARNING, + I18N.getErrorMessage("com.rapidminer.connection.adapter.creation_error", name, e.getMessage()), e); + return null; + } + } + + /** @see #getConfigurableClass() */ + @Override + public boolean canConvert(Object oldConnectionObject) { + return getConfigurableClass().isInstance(oldConnectionObject); + } + + /** + * Converts a {@link ConnectionAdapter} to a {@link ConnectionInformation}. + * + * @param oldConnectionObject + * the adapter to be converted; must not be {@code null} + * @see #getParameterTypes(ParameterHandler) + * @see #parametersByGroup() + */ + @Override + public ConnectionInformation convert(T oldConnectionObject) throws ConversionException { + if (!canConvert(oldConnectionObject)) { + throw new ConversionException("Can not convert " + oldConnectionObject); + } + // find encrypted parameter keys + Map encryptedKeys = getParameterTypes(null).stream() + .filter(p -> p instanceof ParameterTypePassword) + .collect(Collectors.toMap(ParameterType::getKey, p -> p)); + + // build the new connection based on all possible parameters and the configurables actual parameter values + Map parameters = oldConnectionObject.getParameters(); + ConnectionConfigurationBuilder builder = new ConnectionConfigurationBuilder(oldConnectionObject.getName(), getType()); + for (Entry> entry : parametersByGroup().entrySet()) { + List cps = new ArrayList<>(); + for (String p : entry.getValue()) { + boolean isEncrypted = encryptedKeys.containsKey(p); + String value = parameters.get(p); + if (isEncrypted) { + value = encryptedKeys.get(p).transformNewValue(value); + } + cps.add(new ConfigurationParameterImpl(p, value, isEncrypted)); + } + builder.withKeys(entry.getKey(), cps); + } + + return new ConnectionInformationBuilder(builder.build()).build(); + } + + /** @see ConnectionAdapter#getActions() */ + @Override + public Map getAdditionalActions() { + return Collections.emptyMap(); + } + + /** Does nothing by default */ + @Override + public void initialize() { + // noop + } + //endregion + + //region adapter methods + /** + * Gets the {@link ConnectionAdapter} that corresponds to the given {@link ConnectionInformation} and {@link Operator}. + * Uses the {@link ValueProviderHandlerRegistry#injectValues(ConnectionInformation, Operator, boolean) injection mechanism} + * to create a fully functional adapter and {@link #validate(Function, Function) validates} the adapter before + * returning it. If the validation fails, will throw a {@link ConnectionAdapterException} with the given operator, + * connection information's name and type, as well as the {@link ValidationResult}. + * + * @param connection + * the connection; must not be {@code null} + * @param operator + * the operator for context; may be {@code null} + * @return the adapter, never {@code null} + * @throws ConnectionAdapterException + * if a {@link #validate(Function, Function) validation} on the injected values fails + * @throws ConfigurationException + * if an error occurs + * @see ValueProviderHandlerRegistry#injectValues(ConnectionInformation, Operator, boolean) + * @see #create(String, Map) + */ + public T getAdapter(ConnectionInformation connection, Operator operator) throws ConnectionAdapterException, ConfigurationException { + ConnectionConfiguration configuration = ValidationUtil.requireNonNull(connection, "connection").getConfiguration(); + Map keyMap = configuration.getKeyMap(); + + Map valueMap = ValueProviderHandlerRegistry.getInstance().injectValues(connection, operator, false); + ValidationResult validation = validate(valueMap::get, keyMap::get); + if (validation.getType() == ResultType.FAILURE) { + throw new ConnectionAdapterException(operator, configuration, validation); + } + // remove all null values and get rid of group prefix + Map adapterMap = valueMap.entrySet().stream().filter(e -> e.getValue() != null) + .collect(Collectors.toMap(e -> e.getKey().substring(e.getKey().indexOf('.') + 1), Entry::getValue)); + + return create(configuration.getName(), adapterMap); + } + + /** + * Returns a map of group keys to parameter keys. The combined keys of all groups have to contain + * all keys that can be derived from {@link #getParameterTypes(ParameterHandler)}. + * + * @return a singleton map, bundling all parameters in the {@value #BASE_GROUP} group by default + */ + public Map> parametersByGroup() { + return Collections.singletonMap(BASE_GROUP, getParameterTypes(null) + .stream().map(ParameterType::getKey).collect(Collectors.toList())); + } + + /** + * Validates the given {@link ConnectionInformation} based on the {@link ParameterType ParameterTypes} + * retrieved from {@link #getParameterTypes(ParameterHandler)}. + *

+ * Parameters that are not optional according to all parameters set will be checked if they are set. + * If they are not set, that parameter is reported as {@link ValidationResult#I18N_KEY_VALUE_MISSING missing}. + * A parameter is defined as set if a) its value is not null or b) it is + * {@link ConfigurationParameter#isInjected() injected} + * + * @see #validate(Function, Function) + */ + @Override + public ValidationResult validate(ConnectionInformation information) { + if (information == null) { + return ValidationResult.nullable(); + } + Map keyMap = information.getConfiguration().getKeyMap(); + Function configParameters = keyMap::get; + return validate(configParameters.andThen(p -> p.isInjected() ? p.getInjectorName() : p.getValue()), configParameters); + } + + /** + * Tests if the given object is configured correctly. This might fail if injection is involved, since some + * {@link com.rapidminer.connection.valueprovider.ValueProvider ValueProviders} might require an + * {@link Operator Operator} for context. Relies on {@link #validate(Function, Function)} and + * {@link ConnectionAdapter#getTestAction()}. + * + * @see ValueProviderHandlerRegistry#injectValues(ConnectionInformation, Operator, boolean) + */ + @Override + public TestResult test(TestExecutionContext testContext) { + if (testContext == null || testContext.getSubject() == null) { + return TestResult.nullable(); + } + ConnectionInformation connection = testContext.getSubject(); + ConnectionConfiguration configuration = connection.getConfiguration(); + T adapter; + try { + adapter = getAdapter(connection, null); + } catch (ConfigurationException e) { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.connection.adapter.conversion_failed", + new Object[] {configuration.getName(), getType(), e.getLocalizedMessage()}); + return TestResult.failure(TestResult.I18N_KEY_FAILED, e.getLocalizedMessage()); + } catch (ConnectionAdapterException e) { + return TestResult.failure(TestResult.I18N_KEY_INJECTION_FAILURE, + e.getValidation().getParameterErrorMessages(), e.getLocalizedMessage()); + } + + TestConfigurableAction testAction = adapter.getTestAction(); + if (testAction == null) { + return new TestResult(ResultType.NOT_SUPPORTED, TestResult.I18N_KEY_NOT_IMPLEMENTED, null); + } + ActionResult testResult = testAction.doWork(); + switch (testResult.getResult()) { + case SUCCESS: + return TestResult.success(TestResult.I18N_KEY_SUCCESS); + case FAILURE: + String errorMessage = testResult.getMessage(); + LogService.getRoot().log(Level.WARNING, "com.rapidminer.connection.adapter.connection_failed", + new Object[] {configuration.getName(), getType(), errorMessage}); + return TestResult.failure(TestResult.I18N_KEY_FAILED, errorMessage); + case NONE: + default: + return TestResult.nullable(); + } + } + + /** + * Validates the given actual parameters using both the {@link ParameterType ParameterTypes} retrieved from + * {@link #getParameterTypes(ParameterHandler)} as well as the provided + * {@link ConfigurationParameter ConfigurationParameters}. All parameter keys that can be retrieved through + * {@link #parametersByGroup()} will be tested. + *

+ * Parameters that are not optional according to {@link ParameterType#isOptional()} with the given values + * will be checked as follows: + *

    + *
  • Parameters that have no {@link ConfigurationParameter} or are not set according to + * {@link ValidationUtil#isValueSet(ConfigurationParameter)} are marked as + * {@value ValidationResult#I18N_KEY_VALUE_MISSING}
  • + *
  • Parameters that pass the validation util, are not, but still {@code null}, + * are marked as {@value ValidationResult#I18N_KEY_VALUE_MISSING_PLACEHOLDER}
  • + *
  • Parameters that pass the validation util, are {@link ConfigurationParameter#isInjected() injected}, + * but still {@code null}, are marked as {@value ValidationResult#I18N_KEY_VALUE_NOT_INJECTABLE}
  • + *
+ * + * If either of the above is true for any amount of parameters, a {@link ValidationResult} of type + * {@link ResultType#FAILURE} will be returned. Otherwise will return one of type {@link ResultType#SUCCESS}. + * + * @param parameterValues + * a function that maps full parameter keys to their respective values; must not be {@code null} + * @param configParameters + * a function that maps full parameter keys to their respective {@link ConfigurationParameter}; + * must not be {@code null} + * @return a {@link ValidationResult} of either success or failure, + * never {@code null} or of type {@link ResultType#NONE} + */ + protected ValidationResult validate(Function parameterValues, Function configParameters) { + // set up parameter handler with the given values + ParameterHandler paramHandler = new SimpleListBasedParameterHandler() { + @Override + public List getParameterTypes() { + return ConnectionAdapterHandler.this.getParameterTypes(this); + } + }; + // this would potentially override values if there are parameters with the same name in different groups + // configurables by default do not have this problem, since their parameter types are all put together and + // should not have duplicate keys in the first place + parametersByGroup().forEach((group, names) -> names.forEach(name -> + paramHandler.setParameter(name, parameterValues.apply(group + '.' + name)))); + Parameters parameters = paramHandler.getParameters(); + + // go through parameters and collect errors + Map errorMap = new HashMap<>(); + parametersByGroup().forEach((group, names) -> names.forEach(name -> { + ParameterType parameterType = parameters.getParameterType(name); + // ignore irrelevant parameters + if (parameterType == null || parameterType.isOptional()) { + return; + } + String fullKey = group + '.' + name; + ConfigurationParameter parameter = configParameters.apply(fullKey); + // parameter does not exist in connection or is neither set nor injected + if (parameter == null || !ValidationUtil.isValueSet(parameter)) { + errorMap.put(fullKey, ValidationResult.I18N_KEY_VALUE_MISSING); + return; + } + + if (parameters.getParameterOrNull(name) != null) { + return; + } + + String errorString; + if (parameter.isInjected()) { + // parameter is marked as injected but has no value + errorString = ValidationResult.I18N_KEY_VALUE_NOT_INJECTABLE; + } else { + // parameter has placeholders that resolve to null + errorString = ValidationResult.I18N_KEY_VALUE_MISSING_PLACEHOLDER; + } + errorMap.put(fullKey, errorString); + })); + + if (!errorMap.isEmpty()) { + return ValidationResult.failure(ValidationResult.I18N_KEY_FAILURE, errorMap); + } + return ValidationResult.success(ValidationResult.I18N_KEY_SUCCESS); + } + + /** + * Validates the given {@link ConnectionInformation} based on the {@link ParameterType ParameterTypes} + * retrieved from {@link #getParameterTypes(ParameterHandler)} and the given predicate. + *

+ * Parameters that are not optional according to all parameters set will be checked with the predicate. + * If they fail the predicate, that parameter is reported with the provided {@code failureKey}. + * + * @param information + * the connection + * @param validation + * the predicate to determine if a {@link ConfigurationParameter} passes or not + * @param failureKey + * the failure key to use if a parameter fails the validation + * @see ConnectionHandler#validate(Object) ConnectionHandler.validate + */ + protected ValidationResult validate(ConnectionInformation information, + Predicate validation, String failureKey) { + if (information == null || information.getConfiguration() == null) { + return ValidationResult.nullable(); + } + ParameterHandler paramHandler = new SimpleListBasedParameterHandler() { + @Override + public List getParameterTypes() { + return ConnectionAdapterHandler.this.getParameterTypes(this); + } + }; + Map> parametersByGroup = parametersByGroup(); + ConnectionConfiguration configuration = information.getConfiguration(); + parametersByGroup.forEach((group, names) -> names.forEach(name -> + paramHandler.setParameter(name, configuration.getValue(group + '.' + name)))); + + Parameters parameters = paramHandler.getParameters(); + Map errorMap = new HashMap<>(); + parametersByGroup.forEach((group, names) -> names.forEach(name -> { + if (parameters.getParameterType(name).isOptional()) { + return; + } + String fullKey = group + '.' + name; + ConfigurationParameter parameter = configuration.getParameter(fullKey); + if (!validation.test(parameter)) { + errorMap.put(fullKey, failureKey); + } + })); + if (!errorMap.isEmpty()) { + return ValidationResult.failure(ValidationResult.I18N_KEY_FAILURE, errorMap); + } + return ValidationResult.success(ValidationResult.I18N_KEY_SUCCESS); + } + //endregion + + //region static methods to register and retrieve a handler + /** + * Registers the given {@link ConnectionAdapterHandler} and makes it available through {@link #getHandler(String)}. + * Registers the handler with the {@link ConnectionHandlerRegistry} as well as the {@link ConfigurationManager}. + * + * @param handler + * the handler; must not be {@code null} or have an empty {@link #getTypeId() type} + * @param + * the subtype of {@link ConnectionAdapter} that the {@link ConnectionAdapterHandler} can create + * @see ConnectionHandlerRegistry#registerHandler(GenericHandler) + */ + public static synchronized void registerHandler(ConnectionAdapterHandler handler) { + String typeId = ValidationUtil.requireNonNull(handler, "handler").getTypeId(); + ConnectionAdapterHandler registeredhandler = getHandler(typeId, false); + if (registeredhandler != null) { + Level severity = Level.INFO; + String messageKey = "com.rapidminer.connection.adapter.handler_already_registered"; + if (registeredhandler.getClass() != handler.getClass()) { + severity = Level.WARNING; + messageKey += ".mismatch"; + } + LogService.getRoot().log(severity, messageKey, new Object[]{typeId, registeredhandler.getClass(), handler.getClass()}); + // check classes? + return; + } + handlerMap.put(typeId, handler); + handler.initialize(); + // this is now centralized here, so when we decide to completely remove this, we can just do it here + ConfigurationManager.getInstance().register(handler); + ConnectionHandlerRegistry.getInstance().registerHandler(handler); + } + + /** + * Returns the handler for the given type ID. This supports both the old {@link #getTypeId()} + * as well as the new {@link #getType()} style types. Might return {@code null} if no such handler is available. + *

+ * For a handler to be available, it must have been registered using {@link #registerHandler(ConnectionAdapterHandler)}. + * + * @param typeId + * the type of the handler; must not be {@code null}; is of the form {@code [namespace:]type} + * @param + * the expected subtype of {@link ConnectionAdapter} + * @return the registered handler for this type or {@code null} + */ + public static synchronized ConnectionAdapterHandler getHandler(String typeId) { + return getHandler(typeId, true); + } + + /** + * Returns a list with the needed {@link ParameterType ParameterTypes} for the given operator to support connections + * of the given type. If the operator does not implement {@link ConnectionSelectionProvider} or no handler for + * that type was registered, returns a list with only the given parameter. + *

+ * Otherwise the returned list contains the following parameters (in that order) + *

    + *
  1. A {@link ParameterTypeStringCategory dropdown} with {@link #CONNECTION_SOURCE_MODES} to choose the old ro new format
  2. + *
  3. The {@link ParameterTypeConfigurable given parameter}
  4. + *
  5. The {@link com.rapidminer.parameter.ParameterTypeConnectionLocation new parameter} for {@link ConnectionInformation}
  6. + *
+ * The second and third parameters depend on the selection of the first. Furthermore, a {@link ConnectionInformationSelector} + * is installed in the operator (if not already present), which also creates a throughput port for connections. + * The input port from that selector takes precedence over all other parameters. + *

+ * Note: If you want to install the {@link ConnectionInformationSelector} yourself, you have to do + * so before this call, best in the constructor of your operator. Reasons for doing this might be + *

    + *
  • No throughput port wanted
  • + *
  • Positioning of throughput ports
  • + *
  • Subclassing the selector
  • + *
+ * + * @param operator + * the operator these parameters will belong too + * @param typeId + * the type ID for the adapter; must be neither {@code null} nor empty + * @param oldParameter + * the parameter for the {@link Configurable}; must not be{@code null} + * @return the list of parameters, never {@code null} + * @see ConnectionInformationSelector + * @see ConnectionSelectionProvider + */ + public static List getConnectionParameters(Operator operator, String typeId, ParameterTypeConfigurable oldParameter) { + ValidationUtil.requireNonNull(oldParameter, "configurable parameter type"); + + List parameters = new ArrayList<>(3); + parameters.add(oldParameter); + // check if there actually is a handler; if not, just return the normal parameter + ConnectionAdapterHandler handler = getHandler(typeId); + if (handler == null || !(operator instanceof ConnectionSelectionProvider)) { + oldParameter.setOptional(false); + return parameters; + } + // dropdown to select between old and new mode; default depends on compatibility level + ParameterTypeStringCategory connectionSource = new ParameterTypeStringCategory(PARAMETER_CONNECTION_SOURCE, + "select where to look for connections", CONNECTION_SOURCE_MODES, REPOSITORY_MODE, false) { + @Override + public Object getDefaultValue() { + if (operator.getCompatibilityLevel().isAtMost(ConnectionHandlerRegistry.BEFORE_NEW_CONNECTION_MANAGEMENT)) { + return PREDEFINED_MODE; + } + return super.getDefaultValue(); + } + }; + connectionSource.setExpert(false); + parameters.add(0, connectionSource); + + // add condition to old parameter and make optional + oldParameter.registerDependencyCondition(new EqualStringCondition(operator, PARAMETER_CONNECTION_SOURCE, true, PREDEFINED_MODE)); + oldParameter.setOptional(true); + + // install selector if not present + ConnectionSelectionProvider provider = (ConnectionSelectionProvider) operator; + ConnectionInformationSelector cis = provider.getConnectionSelector(); + if (cis == null) { + cis = new ConnectionInformationSelector(operator, handler.getType()); + provider.setConnectionSelector(cis); + cis.makeDefaultPortTransformation(); + operator.getTransformer().addRule(() -> { + Optional.ofNullable(provider.getConnectionSelector()) + .filter(sel -> sel.getInput() == null || !sel.getInput().isConnected()) + .flatMap(suppress(sel -> operator.getParameter(PARAMETER_CONNECTION_SOURCE)).andThen(o -> Optional.of(o == null ? PREDEFINED_MODE : o))) + .filter(PREDEFINED_MODE::equals).map(s -> new SimpleProcessSetupError(Severity.WARNING, operator.getPortOwner(), "connection.deprecated")) + .ifPresent(operator::addError); + }); + } + if (cis.getInput() != null) { + connectionSource.registerDependencyCondition( + new PortConnectedCondition(operator, cis::getInput, false, false)); + } + // get parameters from selector and add dependency condition + List cisTypes = createParameterTypes(cis); + NonEqualStringCondition repoModeCondition = new NonEqualStringCondition(operator, PARAMETER_CONNECTION_SOURCE, true, PREDEFINED_MODE); + cisTypes.stream().peek(p -> p.registerDependencyCondition(repoModeCondition)).forEach(parameters::add); + + return parameters; + } + + /** + * Creates a {@link MDTransformationRule} to check the connection for the given operator. The rule can add + * {@link SimpleProcessSetupError SimpleProcessSetupErrors} if there is no handler for the specific type or + * the selected connection does not fit the wanted type. + * + * @param operator + * the operator to add the rule to; must not be {@code null} + */ + public static MDTransformationRule createProcessSetupRule(O operator) { + return () -> { + ConnectionInformationSelector selector = operator.getConnectionSelector(); + if (selector == null) { + return; + } + ProcessSetupError error = selector.checkConnectionTypeMatch(operator); + if (error == null) { + return; + } + if (error instanceof MetaDataError) { + InputPort input = selector.getInput(); + if (input != null) { + input.addError((MetaDataError) error); + return; + } + } + operator.addError(error); + }; + } + + /** + * Same as {@link #getAdapter(Operator, String, String, RepositoryAccessor) + * getAdapter(operator, oldParameterKey, oldTypeID, operator.getProcess().getRepositoryAccessor())}. + * If the operator is not tied to a process, will use null as the accessor. + */ + public static T getAdapter(Operator operator, String oldParameterKey, String oldTypeID) + throws ConfigurationException, UserError { + Process process = operator.getProcess(); + RepositoryAccessor accessor = process == null ? null : process.getRepositoryAccessor(); + return getAdapter(operator, oldParameterKey, oldTypeID, accessor); + } + + /** + * Looks up the adapter for the given operator. Will resolve where to find it as follows + *
    + *
  1. If the {@link ConnectionInformationSelector#getInput() input port} exists and connected will take + * a {@link ConnectionInformation} from there
  2. + *
  3. If the {@value #REPOSITORY_MODE} is selected for {@value #PARAMETER_CONNECTION_SOURCE}, uses the parameter + * {@value ConnectionInformationSelector#PARAMETER_CONNECTION_ENTRY} to find the {@link ConnectionInformation}
  4. + *
  5. If the {@value #PREDEFINED_MODE} is selected for {@value #PARAMETER_CONNECTION_SOURCE}, uses the parameter + * {@code oldParameterKey} to find the {@link ConfigurationManager#lookup(String, String, RepositoryAccessor) adapter}
  6. + *
+ * + * @param operator + * the operator to check on; must not be {@code null} + * @param oldParameterKey + * the old parameter's key + * @param oldTypeID + * the type ID, not fully qualified + * @param accessor + * the repository accessor; can be {@code null} + * @param + * the expected subtype of {@link Configurable} + * @return the adapter, never {@code null} + * @throws ConnectionAdapterException + * if a {@link #validate(Function, Function) validation} fails in {@link #getAdapter(ConnectionInformation, Operator)} + * @throws UserError + * if other errors occur + * @throws ConfigurationException + * if an error occurred related to the adapter + * @see ConfigurationManager#lookup(String, String, RepositoryAccessor) + * @see ConnectionInformationSelector#getConnection() + */ + @SuppressWarnings("unchecked") + public static T getAdapter(Operator operator, String oldParameterKey, String oldTypeID, RepositoryAccessor accessor) + throws UserError, ConfigurationException { + String connectionSource; + if (ValidationUtil.requireNonNull(operator, "operator").isParameterSet(PARAMETER_CONNECTION_SOURCE)) { + connectionSource = operator.getParameter(PARAMETER_CONNECTION_SOURCE); + } else { + connectionSource = null; + } + boolean inputConnected = false; + ConnectionInformationSelector cis = null; + if (operator instanceof ConnectionSelectionProvider) { + cis = ((ConnectionSelectionProvider) operator).getConnectionSelector(); + inputConnected = cis != null && cis.getInput() != null && cis.getInput().isConnected(); + } + + if (!inputConnected && (cis == null || connectionSource == null || PREDEFINED_MODE.equals(connectionSource))) { + String configurableName = operator.getParameter(oldParameterKey); + T oldConnection = (T) ConfigurationManager.getInstance().lookup(oldTypeID, configurableName, accessor); + ActionStatisticsCollector.INSTANCE.logOldConnection(operator, oldTypeID); + operator.logWarning(I18N.getErrorMessage("process.error.connection.deprecated")); + return oldConnection; + } else { + // make sure data is propagated as otherwise each operator would need to do it himself + cis.passDataThrough(); + ConnectionInformation connection = cis.getConnection(); + ConnectionAdapterHandler handler = ConnectionAdapterHandler.getHandler(cis.getConnectionType()); + if (handler == null) { + throw new UserError(operator, "connection.adapter.no_handler_registered", oldTypeID); + } + T newConnection = (T) handler.getAdapter(connection, operator); + ActionStatisticsCollector.INSTANCE.logNewConnection(operator, connection); + return newConnection; + } + } + + /** + * Returns the handler for the given type ID. This supports both the {@link #getTypeId()} as well as + * the {@link #getType()} style types. Might return {@code null} if no such handler is available. + *

+ * For a handler to be available, it must have been registered using {@link #registerHandler(ConnectionAdapterHandler)}. + * + * @param + * the expected subtype of {@link ConnectionAdapter} + * @param typeId + * the type of the handler; must not be {@code null}; is of the form {@code [namespace:]type} + * @param logNullHandler + * whether or not to log when no handler was registered + * @return the registered handler for this type or {@code null} + */ + @SuppressWarnings("unchecked") + private static synchronized ConnectionAdapterHandler getHandler(String typeId, boolean logNullHandler) { + int separator = ValidationUtil.requireNonEmptyString(typeId, "type ID").indexOf(':'); + if (separator >= 0) { + typeId = typeId.substring(separator + 1); + } + ConnectionAdapterHandler handler = handlerMap.get(typeId); + if (handler == null && logNullHandler) { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.connection.adapter.no_handler_registered", typeId); + } + return (ConnectionAdapterHandler) handler; + } + //endregion + +} diff --git a/src/main/java/com/rapidminer/connection/configuration/ConfigurationParameter.java b/src/main/java/com/rapidminer/connection/configuration/ConfigurationParameter.java new file mode 100644 index 000000000..9d201ff18 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/configuration/ConfigurationParameter.java @@ -0,0 +1,65 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.rapidminer.connection.valueprovider.ValueProviderParameter; + + +/** + * An extension of {@link ValueProviderParameter} that adds the possibility to mark the parameter as being injected. + * {@link ConfigurationParameter ConfigurationParameters} can be grouped together using a {@link ConfigurationParameterGroup}. + * These parameters are like normal parameters for a {@link ConnectionConfiguration}. + * + * @author Jan Czogalla + * @since 9.3 + */ +@JsonDeserialize(as = ConfigurationParameterImpl.class) +public interface ConfigurationParameter extends ValueProviderParameter { + + /** + * Whether this parameter's value should be injected by a + * {@link com.rapidminer.connection.valueprovider.ValueProvider ValueProvider}. + * + * @return by default, checks if {@link #getInjectorName()} is not {@code null} + */ + @JsonIgnore + default boolean isInjected() { + return getInjectorName() != null; + } + + /** + * Returns the name of the {@link com.rapidminer.connection.valueprovider.ValueProvider ValueProvider} that should + * be used for injection. Will return {@code null} if either {@link #isInjected()} returns {@code false} + * or the first matching value provider should be used. + * + * @return the name of the value provider or {@code null} + */ + String getInjectorName(); + + /** + * Set the name of {@link com.rapidminer.connection.valueprovider.ValueProvider ValueProvider} that should + * be used for injection. Can be set to {@code null} to indicate that the first matching value provider should be used. + * + * @param injectorName + * the name of the {@link com.rapidminer.connection.valueprovider.ValueProvider ValueProvider} + */ + void setInjectorName(String injectorName); +} diff --git a/src/main/java/com/rapidminer/connection/configuration/ConfigurationParameterGroup.java b/src/main/java/com/rapidminer/connection/configuration/ConfigurationParameterGroup.java new file mode 100644 index 000000000..aa9a29b81 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/configuration/ConfigurationParameterGroup.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.configuration; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + + +/** + * A group of {@link ConfigurationParameter ConfigurationParameters}. Consists of a non-{@code null}, non-empty group key + * and a non-empty list of parameters. A {@link ConnectionConfiguration} can have several groups to distinguish between + * different parameters. + * + * @author Jan Czogalla + * @see ConnectionConfigurationBuilder#withKeys(Map) + * @see ConnectionConfigurationBuilder#withKeys(String, List) + * @since 9.3 + */ +@JsonDeserialize(as = ConfigurationParameterGroupImpl.class) +public interface ConfigurationParameterGroup { + + /** Get the group key */ + String getGroup(); + + /** Get a copy of the list of parameters */ + List getParameters(); +} diff --git a/src/main/java/com/rapidminer/connection/configuration/ConfigurationParameterGroupImpl.java b/src/main/java/com/rapidminer/connection/configuration/ConfigurationParameterGroupImpl.java new file mode 100644 index 000000000..35e4dc055 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/configuration/ConfigurationParameterGroupImpl.java @@ -0,0 +1,98 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.configuration; + +import static com.rapidminer.connection.valueprovider.ValueProviderParameter.UNIQUE_NAME_COMPARATOR; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.apache.commons.collections4.CollectionUtils; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.rapidminer.tools.ValidationUtil; + + +/** + * Implementation of {@link ConfigurationParameterGroup}. + * + * @author Jan Czogalla + * @since 9.3 + */ +public class ConfigurationParameterGroupImpl implements ConfigurationParameterGroup { + + private String group; + private List parameters; + + /** Minimal constructor. Both arguments must not be {@code null} or empty. */ + @JsonCreator + public ConfigurationParameterGroupImpl(@JsonProperty(value = "group", required = true) String group, + @JsonProperty(value = "parameters", required = true) List parameters) { + this.setGroup(group); + this.setParameters(parameters); + } + + @Override + public String getGroup() { + return group; + } + + /** + * Sets the group key. Is only used during creation (either programmatically or when parsing from Json). The group key + * must not be {@code null} or empty and must not contain a dot. + */ + private void setGroup(String group) { + this.group = ValidationUtil.requireNoDot(ValidationUtil.requireNonEmptyString(group, "group"), "group"); + } + + @Override + public List getParameters() { + return new ArrayList<>(parameters); + } + + /** + * Sets the list of parameters. Is only used during creation (either programmatically or when parsing from Json). + * The list must not be {@code null}, or only filled with {@code null} elements. + */ + private void setParameters(List parameters) { + parameters = ValidationUtil.stripToEmptyList(parameters); + ValidationUtil.noDuplicatesAllowed(parameters, UNIQUE_NAME_COMPARATOR, "parameters"); + this.parameters = parameters; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConfigurationParameterGroupImpl that = (ConfigurationParameterGroupImpl) o; + return Objects.equals(group, that.group) && + CollectionUtils.isEqualCollection(parameters, that.parameters); + } + + @Override + public int hashCode() { + return Objects.hash(group, parameters); + } +} diff --git a/src/main/java/com/rapidminer/connection/configuration/ConfigurationParameterImpl.java b/src/main/java/com/rapidminer/connection/configuration/ConfigurationParameterImpl.java new file mode 100644 index 000000000..fb5f8df19 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/configuration/ConfigurationParameterImpl.java @@ -0,0 +1,111 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.configuration; + +import java.util.Objects; + +import org.apache.commons.lang.StringUtils; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.rapidminer.connection.valueprovider.ValueProviderParameterImpl; + +/** + * Implementation of {@link ConfigurationParameter} and subclass of {@link ValueProviderParameterImpl}. + * Injected parameters will always return a {@code null} value, and will not allow to set the value while injected. + * The old value is kept though, to be reused if the injected flag changes. + * + * @author Jan Czogalla + * @since 9.3 + */ +public class ConfigurationParameterImpl extends ValueProviderParameterImpl implements ConfigurationParameter { + + private String injectorName; + + /** Minimal constructor */ + @JsonCreator + public ConfigurationParameterImpl(@JsonProperty(value = "name", required = true) String name) { + super(name); + } + + /** Key/value constructor. {@code value} cannot be {@code null} or empty here. */ + public ConfigurationParameterImpl(String name, String value) { + super(name, value); + } + + /** Constructor for enabled parameters. Only {@code name} is mandatory here. */ + public ConfigurationParameterImpl(String name, String value, boolean encrypted) { + super(name, value, encrypted); + } + + /** Full constructor. Only {@code name} is mandatory here. */ + public ConfigurationParameterImpl(String name, String value, boolean encrypted, String injectorName, boolean enabled) { + super(name, value, encrypted, enabled); + setInjectorName(injectorName); + } + + /** @return the value iff {@link #isInjected()} returns {@code false}, {@code null} otherwise */ + @Override + public String getValue() { + return isInjected() ? null : super.getValue(); + } + + /** + * {@inheritDoc} + *

+ * Will do nothing if the parameter should be injected. + */ + @Override + public void setValue(String value) { + if (isInjected()) { + return; + } + super.setValue(value); + } + + @Override + public String getInjectorName() { + return injectorName; + } + + @Override + public void setInjectorName(String injectorName) { + this.injectorName = StringUtils.trimToNull(injectorName); + if (this.injectorName != null) { + super.setValue(null); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!super.equals(o)) { + return false; + } + ConfigurationParameterImpl that = (ConfigurationParameterImpl) o; + return Objects.equals(injectorName, that.injectorName); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), injectorName); + } +} diff --git a/src/main/java/com/rapidminer/connection/configuration/ConnectionConfiguration.java b/src/main/java/com/rapidminer/connection/configuration/ConnectionConfiguration.java new file mode 100644 index 000000000..4536a7eae --- /dev/null +++ b/src/main/java/com/rapidminer/connection/configuration/ConnectionConfiguration.java @@ -0,0 +1,149 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.configuration; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.rapidminer.connection.valueprovider.ValueProvider; + + +/** + * An interface for a general connection configuration. A configuration consists of at least a name, a type and an (unique) id, + * which also should make it uniquely identifiable. + *

+ * Additional information can be added, like a description or tags. + *

+ * Optional configuration elements are a list of {@link ValueProvider ValueProviders}, {@link ConfigurationParameterGroup ConfigurationParameterGroups} + * and {@link PlaceholderParameter PlaceholderConfigurationParameters}. The keys and placeholders must be unique + * as defined by {@link com.rapidminer.connection.valueprovider.ValueProviderParameter#UNIQUE_NAME_COMPARATOR ValueProviderParameter.UNIQUE_NAME_COMPARATOR}. + *

+ * New instances can be created using a {@link ConnectionConfigurationBuilder}. + * + * @author Jan Czogalla + * @since 9.3 + */ +@JsonDeserialize(as = ConnectionConfigurationImpl.class) +public interface ConnectionConfiguration { + + /** Gets the name */ + String getName(); + + /** Gets the description */ + String getDescription(); + + /** + * Sets the description + * + * @param description + * the description; will be stripped to an empty string + */ + void setDescription(String description); + + /** Gets a copy of the list of tags */ + List getTags(); + + /** + * Sets the list of tags + * + * @param tags + * the tags; will be {@link com.rapidminer.tools.ValidationUtil#stripToEmptyList(List, java.util.function.Predicate) stripped to an empty list} + */ + void setTags(List tags); + + /** Gets the type */ + String getType(); + + /** Gets the id */ + String getId(); + + /** Gets a copy of the list of {@link ValueProvider ValueProviders} */ + List getValueProviders(); + + /** Gets a copy of the map of value provider names to value providers */ + Map getValueProviderMap(); + + /** Gets a copy of the list of {@link ConfigurationParameterGroup ConfigurationParameterGroups} */ + List getKeys(); + + /** Gets a copy of the map of fully qualified parameter keys to parameters */ + Map getKeyMap(); + + /** Gets a copy of the list of {@link PlaceholderParameter PlaceholderConfigurationParameters} */ + List getPlaceholders(); + + /** Gets a copy of the map of fully qualified placeholder parameters to parameters */ + Map getPlaceholderKeyMap(); + + /** Checks wether the specified fully qualified key represents a placeholder parameter. */ + boolean isPlaceholder(String key); + + /** Gets a list of all qualified keys, both normal keys and placeholders; changes on this list have no effect on this configuration */ + List getAllParameterKeys(); + + /** + * Gets the {@link ConfigurationParameter} associated with the specified fully qualified key; + * will return {@code null} for invalid keys. The returned {@link ConfigurationParameter} is modifiable + */ + ConfigurationParameter getParameter(String key); + + /** + * Gets the value of the {@link ConfigurationParameter} represented by the specified fully qualified key if present. + * May return {@code null} when there is no parameter with that key, the value is not set or the value is to be + * injected. + * + * @see ConfigurationParameter#getValue() + */ + default String getValue(String key) { + ConfigurationParameter parameter = getParameter(key); + return parameter == null ? null : parameter.getValue(); + } + + /** + * Checks whether the value associated with the specified fully qualified key is set. + * + * @return by default, returns {@code true} iff {@link #getValue(String)} does not return {@code null} + */ + default boolean isValueSet(String key) { + return getValue(key) != null; + } + + /** + * Checks whether the {@link ConfigurationParameter} represented by the specified fully qualified key is to be injected. + * + * @return by default, returns {@code true} iff a parameter is associated with the key and + * {@link ConfigurationParameter#isInjected()} returns {@code true} + */ + default boolean isValueInjected(String key) { + ConfigurationParameter parameter = getParameter(key); + return parameter != null && parameter.isInjected(); + } + + /** + * Checks whether the {@link ConfigurationParameter} represented by the specified fully qualified key is encrypted. + * + * @return by default, returns {@code true} iff a parameter is associated with the key and + * {@link ConfigurationParameter#isEncrypted()} returns {@code true} + */ + default boolean isValueEncrypted(String key) { + ConfigurationParameter parameter = getParameter(key); + return parameter != null && parameter.isEncrypted(); + } +} diff --git a/src/main/java/com/rapidminer/connection/configuration/ConnectionConfigurationBuilder.java b/src/main/java/com/rapidminer/connection/configuration/ConnectionConfigurationBuilder.java new file mode 100644 index 000000000..1f230174a --- /dev/null +++ b/src/main/java/com/rapidminer/connection/configuration/ConnectionConfigurationBuilder.java @@ -0,0 +1,233 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.configuration; + +import static com.rapidminer.connection.valueprovider.ValueProviderParameter.UNIQUE_NAME_COMPARATOR; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.rapidminer.connection.ConnectionInformationSerializer; +import com.rapidminer.tools.ValidationUtil; +import com.rapidminer.connection.valueprovider.ValueProvider; + + +/** + * Builder for {@link ConnectionConfiguration ConnectionConfigurations}. Instances cannot be reused. + * + * @author Jan Czogalla + * @since 9.3 + */ +public class ConnectionConfigurationBuilder { + + private ConnectionConfigurationImpl object; + + private static final ObjectReader reader; + private static final ObjectWriter writer; + + static { + ObjectMapper mapper = ConnectionInformationSerializer.getRemoteObjectMapper(); + reader = mapper.reader(ConnectionConfigurationImpl.class); + writer = mapper.writerWithType(ConnectionConfigurationImpl.class); + } + + /** + * Minimal constructor + * + * @param name + * the name; must be neither {@code null} nor empty + * @param type + * the type; must be neither {@code null} nor empty + */ + public ConnectionConfigurationBuilder(String name, String type) { + object = new ConnectionConfigurationImpl(name, type); + } + + /** + * Create a builder based on an existing {@link ConnectionConfiguration}. + * + * @param original + * the original configuration + * @throws IOException + * if the configuration could not be parsed with jackson + * @throws IllegalArgumentException + * if parameter original is {@code null} or empty + */ + public ConnectionConfigurationBuilder(ConnectionConfiguration original) throws IOException { + ValidationUtil.requireNonNull(original, "original connection configuration"); + // create a copy using jackson + object = reader.readValue(writer.writeValueAsBytes(original)); + } + + /** + * Create a builder based on an existing {@link ConnectionConfiguration}, but with a new name. + * + * @param original + * the original configuration + * @param newName + * the new name for the configuration + * @throws IOException + * if the configuration could not be parsed with jackson + * @throws IllegalArgumentException + * if parameter original is {@code null} or empty + */ + public ConnectionConfigurationBuilder(ConnectionConfiguration original, String newName) throws IOException { + this(original); + object.name = ValidationUtil.requireNonEmptyString(newName, "new name"); + } + + /** + * Set the description for the object to be built. + * + * @param description + * the description; will be stripped to an empty string + */ + public ConnectionConfigurationBuilder withDescription(String description) { + object.setDescription(description); + return this; + } + + /** + * Set the tags for the object to be built. + * + * @param tags + * the tags; will be {@link ValidationUtil#stripToEmptyList(List, java.util.function.Predicate) stripped to an empty list} + */ + public ConnectionConfigurationBuilder withTags(List tags) { + object.setTags(tags); + return this; + } + + /** + * Add a tag to the list for the object to be built. + * + * @param tag + * a tag to add; must be neither {@code null} nor empty + */ + public ConnectionConfigurationBuilder withTag(String tag) { + object.tags.add(ValidationUtil.requireNonEmptyString(tag, "tag")); + return this; + } + + /** + * Set the value providers for the object to be built. + * + * @param valueProviders + * the list of value providers; will be {@link ValidationUtil#stripToEmptyList(List) stripped to an empty list} + */ + public ConnectionConfigurationBuilder withValueProviders(List valueProviders) { + object.setValueProviders(valueProviders); + return this; + } + + /** + * Add a value provider to the list for the object to be built. + * + * @param valueProvider + * a value provider to add; must not be {@code null} + */ + public ConnectionConfigurationBuilder withValueProvider(ValueProvider valueProvider) { + object.valueProviders.add(ValidationUtil.requireNonNull(valueProvider, "value provider")); + return this; + } + + /** + * Set the parameter groups for the object to be built. + * + * @param keys + * a map of group key/list of parameters + */ + public ConnectionConfigurationBuilder withKeys(Map> keys) { + if (keys != null) { + object.setKeys(keys.entrySet().stream().map(e -> new ConfigurationParameterGroupImpl(e.getKey(), e.getValue())).collect(Collectors.toList())); + } + return this; + } + + /** + * Add a parameter group to the list for the object to be built. Keys will be merged if the group already exists. + * + * @param group + * the group the keys belong to; must be neither {@code null} nor empty + * @param keys + * the list of keys for the given group; must not be {@code null} and contain at least one non-{@code null} element + */ + public ConnectionConfigurationBuilder withKeys(String group, List keys) { + List strippedKeys = ValidationUtil.stripToEmptyList(keys); + additiveCheckForDuplicates(group, strippedKeys, "keys"); + object.keys.stream().filter(cg -> cg.getGroup().equals(group)).findFirst().ifPresent(g -> { + strippedKeys.addAll(g.getParameters()); + object.keys.remove(g); + }); + ConfigurationParameterGroupImpl configGroup = new ConfigurationParameterGroupImpl(group, strippedKeys); + object.keys.add(configGroup); + return this; + } + + /** + * Set the list of placeholder parameters for the object to be built. + * + * @param placeholders + * the list of placeholders; must not be {@code null} and contain at least one non-{@code null} element + */ + public ConnectionConfigurationBuilder withPlaceholders(List placeholders) { + object.setPlaceholders(placeholders); + return this; + } + + /** + * Add a placeholder parameter to the list for the object to be built. + * + * @param placeholder + * the placeholder to add; must not be {@code null} + */ + public ConnectionConfigurationBuilder withPlaceholder(PlaceholderParameter placeholder) { + ValidationUtil.requireNonNull(placeholder, "placeholder"); + additiveCheckForDuplicates(placeholder.getGroup(), Collections.singletonList(placeholder), "placeholder"); + object.placeholders.add(placeholder); + return this; + } + + /** + * Build and return the new {@link ConnectionConfiguration}. Afterwards, the builder becomes invalid. + */ + public ConnectionConfiguration build() { + ConnectionConfiguration configuration = object; + object = null; + return configuration; + } + + /** + * Checks for duplicates in case of additive methods + * + * @see #withKeys(String, List) + * @see #withPlaceholder(PlaceholderParameter) + */ + private void additiveCheckForDuplicates(String group, List keys, String name) { + List placeholderList = object.placeholders.stream().filter(p -> p.getGroup().equals(group)).collect(Collectors.toList()); + List keyList = object.keys.stream().filter(cpg -> cpg.getGroup().equals(group)).flatMap(cg -> cg.getParameters().stream()).collect(Collectors.toList()); + ValidationUtil.noDuplicatesAllowed(keys, UNIQUE_NAME_COMPARATOR, name, placeholderList, keyList); + } +} diff --git a/src/main/java/com/rapidminer/connection/configuration/ConnectionConfigurationImpl.java b/src/main/java/com/rapidminer/connection/configuration/ConnectionConfigurationImpl.java new file mode 100644 index 000000000..83f9e7242 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/configuration/ConnectionConfigurationImpl.java @@ -0,0 +1,284 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.configuration; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.UUID; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang.StringUtils; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.rapidminer.tools.ValidationUtil; +import com.rapidminer.connection.valueprovider.ValueProvider; +import com.rapidminer.tools.container.Pair; + + +/** + * Implementation of {@link ConnectionConfiguration}. New instances can be created using the {@link ConnectionConfigurationBuilder}. + * + * @author Jan Czogalla + * @since 9.3 + */ +public class ConnectionConfigurationImpl implements ConnectionConfiguration { + + private static final Comparator> PAIR_COMPARATOR = Comparator., String>comparing(Pair::getFirst).thenComparing(Pair::getSecond); + + String name; + private String description = ""; + List tags = new ArrayList<>(); + + private String type; + private String id = UUID.randomUUID().toString(); + + List valueProviders = new ArrayList<>(); + List keys = new ArrayList<>(); + List placeholders = new ArrayList<>(); + + private Map keyMap = new TreeMap<>(); + private Map placeholderKeyMap = new TreeMap<>(); + private Map valueProviderMap = new LinkedHashMap<>(); + + /** + * Minimal constructor + */ + @JsonCreator + public ConnectionConfigurationImpl(@JsonProperty(value = "name", required = true) String name, + @JsonProperty(value = "type", required = true) String type) { + this.setName(name); + this.setType(type); + } + + @Override + public String getName() { + return name; + } + + /** + * Sets the name. Is only used during creation (either programmatically or when parsing from Json). The name + * must not be {@code null} or empty. + */ + private void setName(String name) { + this.name = ValidationUtil.requireNonEmptyString(name, "name"); + } + + @Override + public String getDescription() { + return description; + } + + @Override + public void setDescription(String description) { + this.description = StringUtils.stripToEmpty(description); + } + + @Override + public List getTags() { + return new ArrayList<>(tags); + } + + @Override + public void setTags(List tags) { + this.tags = ValidationUtil.stripToEmptyList(tags, s -> !s.isEmpty()); + } + + @Override + public String getType() { + return type; + } + + /** + * Sets the type. Is only used during creation (either programmatically or when parsing from Json). The type + * must not be {@code null} or empty. + */ + private void setType(String type) { + this.type = ValidationUtil.requireNonEmptyString(type, "type"); + } + + @Override + public String getId() { + return id; + } + + /** + * Sets the id. Is only used during creation (either programmatically or when parsing from Json). The id + * must not be {@code null} or empty. + */ + private void setId(String id) { + this.id = ValidationUtil.requireNonEmptyString(id, "id"); + } + + @Override + public List getValueProviders() { + return new ArrayList<>(valueProviders); + } + + @Override + @JsonIgnore + public Map getValueProviderMap() { + ensureProviderMap(); + return new LinkedHashMap<>(valueProviderMap); + } + + private synchronized void ensureProviderMap() { + if (valueProviderMap.isEmpty() && !valueProviders.isEmpty()) { + valueProviders.forEach(vp -> valueProviderMap.putIfAbsent(vp.getName(), vp)); + } + } + + + /** + * Sets the list of value providers. Is only used during creation (either programmatically or when parsing from Json). + */ + void setValueProviders(List valueProviders) { + this.valueProviders = ValidationUtil.stripToEmptyList(valueProviders); + } + + @Override + public List getKeys() { + return new ArrayList<>(keys); + } + + @Override + @JsonIgnore + public Map getKeyMap() { + ensureKeyMap(); + return new TreeMap<>(keyMap); + } + + private synchronized void ensureKeyMap() { + if (keyMap.isEmpty() && !keys.isEmpty()) { + populateKeyMap(keys, keyMap); + } + } + + private void populateKeyMap(List configGroups, Map configMap) { + configGroups.stream().flatMap(cg -> cg.getParameters().stream().map(cp -> new Pair<>(cg.getGroup() + '.' + cp.getName(), cp))) + .forEach(p -> configMap.putIfAbsent(p.getFirst(), p.getSecond())); + } + + /** + * Sets the list of parameter groups. Is only used during creation (either programmatically or when parsing from Json). + */ + void setKeys(List keys) { + keys = ValidationUtil.stripToEmptyList(keys); + checkKeysForDuplicates(keys, placeholders, "keys"); + this.keys = keys; + this.keyMap.clear(); + } + + @Override + public List getPlaceholders() { + return new ArrayList<>(placeholders); + } + + @Override + @JsonIgnore + public Map getPlaceholderKeyMap() { + ensurePlaceholderKeyMap(); + return new TreeMap<>(placeholderKeyMap); + } + + private synchronized void ensurePlaceholderKeyMap() { + if (placeholderKeyMap.isEmpty() && !placeholders.isEmpty()) { + populatePlaceholderKeyMap(placeholders, placeholderKeyMap); + } + } + + private void populatePlaceholderKeyMap(List configGroups, Map configMap) { + configGroups.stream().map(acp -> new Pair<>(acp.getGroup() + '.' + acp.getName(), acp)) + .forEach(p -> configMap.putIfAbsent(p.getFirst(), p.getSecond())); + } + + /** + * Sets the list of placeholder parameters. Is only used during creation (either programmatically or when parsing from Json). + */ + void setPlaceholders(List placeholders) { + placeholders = ValidationUtil.stripToEmptyList(placeholders); + checkKeysForDuplicates(keys, placeholders, "placeholders"); + this.placeholders = placeholders; + this.placeholderKeyMap.clear(); + } + + @Override + public boolean isPlaceholder(String key) { + ensurePlaceholderKeyMap(); + return placeholderKeyMap.containsKey(key); + } + + @Override + @JsonIgnore + public List getAllParameterKeys() { + ensureKeyMap(); + ensurePlaceholderKeyMap(); + ArrayList parameterKeys = new ArrayList<>(keyMap.size() + placeholderKeyMap.size()); + parameterKeys.addAll(keyMap.keySet()); + parameterKeys.addAll(placeholderKeyMap.keySet()); + return parameterKeys; + } + + @Override + public ConfigurationParameter getParameter(String key) { + ensureKeyMap(); + ensurePlaceholderKeyMap(); + return keyMap.getOrDefault(key, placeholderKeyMap.get(key)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConnectionConfigurationImpl that = (ConnectionConfigurationImpl) o; + return Objects.equals(id, that.id) && + Objects.equals(type, that.type) && + Objects.equals(description, that.description) && + CollectionUtils.isEqualCollection(tags, that.tags) && + CollectionUtils.isEqualCollection(valueProviders, that.valueProviders) && + CollectionUtils.isEqualCollection(keys, that.keys) && + CollectionUtils.isEqualCollection(placeholders, that.placeholders); + } + + @Override + public int hashCode() { + return Objects.hash(id, description, tags, type, valueProviders, keys, placeholders); + } + + /** + * Checks keys and placeholders for duplicates. + */ + private void checkKeysForDuplicates(List keys, List placeholders, String name) { + List> allParameters = new ArrayList<>(); + keys.forEach(group -> group.getParameters().forEach(p -> allParameters.add(new Pair<>(group.getGroup(), p.getName())))); + placeholders.stream().map(p -> new Pair<>(p.getGroup(), p.getName())).forEach(allParameters::add); + ValidationUtil.noDuplicatesAllowed(allParameters, PAIR_COMPARATOR, name); + } + +} diff --git a/src/main/java/com/rapidminer/connection/configuration/PlaceholderParameter.java b/src/main/java/com/rapidminer/connection/configuration/PlaceholderParameter.java new file mode 100644 index 000000000..e90544f0a --- /dev/null +++ b/src/main/java/com/rapidminer/connection/configuration/PlaceholderParameter.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.configuration; + +import java.util.List; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + + +/** + * An extension of {@link ConfigurationParameter} that adds a group key to single out specific parameters in a {@link ConnectionConfiguration}. + * + * @author Jan Czogalla + * @see ConnectionConfigurationBuilder#withPlaceholders(List) + * @see ConnectionConfigurationBuilder#withPlaceholder(PlaceholderParameter) + * @since 9.3 + */ +@JsonDeserialize(as = PlaceholderParameterImpl.class) +public interface PlaceholderParameter extends ConfigurationParameter { + + /** Get the group key of this parameter */ + String getGroup(); +} diff --git a/src/main/java/com/rapidminer/connection/configuration/PlaceholderParameterImpl.java b/src/main/java/com/rapidminer/connection/configuration/PlaceholderParameterImpl.java new file mode 100644 index 000000000..0b86c1aca --- /dev/null +++ b/src/main/java/com/rapidminer/connection/configuration/PlaceholderParameterImpl.java @@ -0,0 +1,85 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.configuration; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.rapidminer.tools.ValidationUtil; + + +/** + * Implementation of {@link PlaceholderParameter} and a subclass of {@link ConfigurationParameterImpl}. + * + * @author Jan Czogalla + * @since 9.3 + */ +public class PlaceholderParameterImpl extends ConfigurationParameterImpl implements PlaceholderParameter { + + private String group; + + /** Minimal constructor */ + @JsonCreator + public PlaceholderParameterImpl(@JsonProperty(value = "name", required = true) String name, + @JsonProperty(value = "group", required = true) String group) { + this(name, group, null); + } + + /** Constructor for enabled parameters. Only {@code name} and {@code group} are mandatory here. */ + public PlaceholderParameterImpl(String name, String group, String injectorName) { + this(name, null, group, false, injectorName, true); + } + + /** Full constructor. Only {@code name} and {@code group} are mandatory here. */ + public PlaceholderParameterImpl(String name, String value, String group, boolean encrypted, String injectorName, boolean enabled) { + super(name, value, encrypted, injectorName, enabled); + setGroup(group); + } + + @Override + public String getGroup() { + return group; + } + + /** + * Sets the group key. Is only used during creation (either programmatically or when parsing from Json). The group key + * must not be {@code null} or empty and must not contain a dot. + */ + private void setGroup(String group) { + this.group = ValidationUtil.requireNoDot(ValidationUtil.requireNonEmptyString(group, "group"), "group"); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!super.equals(o)) { + return false; + } + PlaceholderParameterImpl that = (PlaceholderParameterImpl) o; + return Objects.equals(group, that.group); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), group); + } +} \ No newline at end of file diff --git a/src/main/java/com/rapidminer/connection/gui/AbstractConnectionGUI.java b/src/main/java/com/rapidminer/connection/gui/AbstractConnectionGUI.java new file mode 100644 index 000000000..2c00ddf97 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/AbstractConnectionGUI.java @@ -0,0 +1,467 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General + * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui; + +import java.awt.BorderLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Window; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import javax.swing.BorderFactory; +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JPanel; +import javax.swing.JSeparator; +import javax.swing.JTabbedPane; +import javax.swing.border.Border; + +import com.rapidminer.connection.ConnectionHandlerRegistry; +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.gui.components.ConnectionInfoPanel; +import com.rapidminer.connection.gui.components.ConnectionSourcesPanel; +import com.rapidminer.connection.gui.components.InjectionPanel; +import com.rapidminer.connection.gui.model.ConnectionModel; +import com.rapidminer.connection.gui.model.ConnectionModelConverter; +import com.rapidminer.connection.gui.model.ConnectionParameterGroupModel; +import com.rapidminer.connection.gui.model.ConnectionParameterModel; +import com.rapidminer.connection.gui.model.ValueProviderModel; +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.gui.tools.ExtendedJTabbedPane; +import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.gui.tools.dialogs.ConfirmDialog; +import com.rapidminer.repository.RepositoryLocation; + + +/** + * Abstract UI for editing connections. It takes care of providing elements that are always the same and therefore + * covered by the RapidMiner Studio core, like the injection configuration and the tab structure for groups. However, + * some behavior can be changed by overriding the protected methods. + * + * @author Jonas Wilms-Pfau + * @since 9.3.0 + */ +public abstract class AbstractConnectionGUI implements ConnectionGUI { + + /** + * Default horizontal distance between components + */ + public static final int HORIZONTAL_COMPONENT_SPACING = 16; + + /** + * Default vertical distance between components + */ + public static final int VERTICAL_COMPONENT_SPACING = 12; + + /** + * Default spacing used by the tab panels + */ + public static final Border DEFAULT_PANEL_BORDER = BorderFactory.createEmptyBorder(16, 32, 0, 16); + + /** + * The description string used in the tooltip + */ + private static final String TOOLTIP_DESCRIPTION = ConnectionI18N.getConnectionGUILabel("tooltip.description"); + /** + * The full key string used in the tooltip + */ + private static final String TOOLTIP_FULL_KEY = ConnectionI18N.getConnectionGUILabel("tooltip.full_key"); + + /** Closing tags that would stop the Swing HTML parser */ + private static final Pattern CLOSING_TAGS = Pattern.compile("", Pattern.CASE_INSENSITIVE); + + /** + * the connection object + */ + protected ConnectionInformation connection; + + /** + * the connection model + */ + protected ConnectionModel connectionModel; + + /** + * the tabbed pane that contains the info tab and all connection specific tabs + */ + private final JTabbedPane tabbedPane = new ExtendedJTabbedPane(); + + /** the parent window */ + private final Window parent; + + /** + * the parent dialog + */ + private final JDialog parentDialog; + + /** + * Creates a new abstract connection gui + * @param parent + * the parent window + * @param connection + * the edited connection + * @param location + * the location of the connection + * @param editable + * if this UI is in edit mode ({@code true}) or view mode ({@code false}) + */ + public AbstractConnectionGUI(Window parent, ConnectionInformation connection, + RepositoryLocation location, boolean editable) { + this.connection = connection; + this.connectionModel = ConnectionModelConverter.fromConnection(connection, location, editable); + this.parent = parent; + this.parentDialog = parent instanceof JDialog ? (JDialog) parent : null; + } + + /** + * Returns the connection edit dialog + * + * @return the outer dialog + */ + protected Window getParent() { + return parent; + } + + /** + * Returns the connection edit dialog + * + * @return the outer dialog + */ + protected JDialog getParentDialog() { + return parentDialog; + } + + @Override + public ConnectionInformation getConnection() { + return ConnectionModelConverter.applyConnectionModel(connection, connectionModel); + } + + @Override + public synchronized JComponent getConnectionEditComponent() { + // this method is only triggered once + getTabbedPane().removeAll(); + addInfoTab(); + addGroupTabs(); + addSourcesTab(); + return getTabbedPane(); + } + + /** + * Returns the injectable parameters the the given group + * + * @param group + * the group that is displayed + * @return the injectable parameter for the group + */ + @Override + public List getInjectableParameters(ConnectionParameterGroupModel group) { + return group.getParameters(); + } + + /** + * Returns the {@link JComponent} for a group that is used inside a tabbed pane. + *

The tab title is queried via the composite i18n key {@code gui.label.connection.group.{type}.{group}.label} + *

+ * + * @param groupModel + * the model for the group that is displayed in this tab + * @param connectionModel + * the model for the entire connection + * @return the component for this group tab, or {@code null} iff that group does not need a UI for whatever reason + */ + protected abstract JComponent getComponentForGroup(ConnectionParameterGroupModel groupModel, ConnectionModel connectionModel); + + /** + * Returns the panel for the first tab "Info". Override / extend if the default info tab does not fully cover your + * needs. + *

+ * Note: The general layout of the info tab should not be changed. + *

+ * + * @return the panel for the first tab + */ + protected JPanel getInfoPanel() { + return new ConnectionInfoPanel(getConnectionModel()); + } + + /** + * The model backing the connection UI. + * + * @return the connection model, never {@code null} + */ + protected ConnectionModel getConnectionModel() { + return connectionModel; + } + + /** + * Returns the tabbed pane + * + * @return the {@link JTabbedPane} + */ + private JTabbedPane getTabbedPane() { + return tabbedPane; + } + + /** + * The injection panel contains the injection button and the help text + * + * @return the injection panel + */ + private JPanel getInjectionPanel(Supplier> injectableParameters) { + return new InjectionPanel(getParentDialog(), getConnectionModel().getType(), injectableParameters, getConnectionModel().getValueProviders(), this::setInjected); + } + + /** + * Adds the info tab to the tabbed pane + */ + private void addInfoTab() { + getTabbedPane().addTab(ConnectionI18N.getConnectionGUILabel("info_panel"), getInfoPanel()); + } + + /** + * Adds all connection specific tabs + */ + private void addGroupTabs() { + for (ConnectionParameterGroupModel group : getDisplayedGroups()) { + addInjectableTab(group, getComponentForGroup(group, connectionModel)); + } + } + + /** + * @return the displayed groups, or an empty list if the connection type is unknown + */ + protected List getDisplayedGroups() { + if (!ConnectionHandlerRegistry.getInstance().isTypeKnown(getConnectionModel().getType())) { + return Collections.emptyList(); + } + return getConnectionModel().getParameterGroups(); + } + + /** + * Adds a tab that contains the given component as well as the injection button on the bottom + * + * @param group + * the group that is used for i18n and {@link #getInjectableParameters(ConnectionParameterGroupModel)} + * @param component + * the content of the tab without the inject functionality + */ + protected void addInjectableTab(ConnectionParameterGroupModel group, JComponent component) { + JPanel panelAndInject = new JPanel(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + if (component == null) { + return; + } + + c.anchor = GridBagConstraints.WEST; + c.weighty = 1; + c.weightx = 1; + c.fill = GridBagConstraints.BOTH; + c.gridx = 0; + c.anchor = GridBagConstraints.NORTHWEST; + panelAndInject.add(component, c); + + if (getConnectionModel().isEditable()) { + c.weighty = 0; + panelAndInject.add(new JSeparator(JSeparator.HORIZONTAL), c); + + c.anchor = GridBagConstraints.SOUTHWEST; + panelAndInject.add(getInjectionPanel(() -> getInjectableParameters(group)), c); + } + + String typeKey = getConnectionModel().getType(); + String groupKey = group.getName(); + String title = ConnectionI18N.getGroupName(typeKey, groupKey, ConnectionI18N.LABEL_SUFFIX, groupKey); + String tip = ConnectionI18N.getGroupName(typeKey, groupKey, ConnectionI18N.LABEL_SUFFIX, null); + Icon icon = ConnectionI18N.getGroupIcon(typeKey, groupKey); + + getTabbedPane().addTab(title, icon, panelAndInject, tip); + } + + @Override + public void validationResult(Map parameterErrorMap) { + // set potential errors + for (ConnectionParameterGroupModel parameterGroup : getConnectionModel().getParameterGroups()) { + for (ConnectionParameterModel parameter : parameterGroup.getParameters()) { + String fullKey = parameterGroup.getName() + "." + parameter.getName(); + parameter.validationErrorProperty().setValue(parameterErrorMap.getOrDefault(fullKey, null)); + } + } + } + + @Override + public boolean preSaveCheck() { + List configuredUnusedVPs = + connectionModel.valueProvidersProperty().stream() + // filter vps that have set parameters + .filter(vp -> vp.getParameters().stream() + .anyMatch(p -> p.getValue() != null && !p.getValue().isEmpty())) + // filter vps that are not used + .filter(vp -> connectionModel.getParameterGroups().stream() + .noneMatch(group -> group.getParameters().stream() + .anyMatch(param -> vp.getName().equals(param.getInjectorName())))) + .map(ValueProviderModel::getName) + .collect(Collectors.toList()); + if (configuredUnusedVPs.isEmpty()) { + return true; + } + String vpsString = configuredUnusedVPs.get(0); + if (configuredUnusedVPs.size() > 1) { + vpsString += ", " + configuredUnusedVPs.get(1); + if (configuredUnusedVPs.size() > 2) { + vpsString += ", " + configuredUnusedVPs.get(2); + if (configuredUnusedVPs.size() > 3) { + vpsString += ", ..."; + } + } + } + String finalArgument = vpsString; + return SwingTools.invokeAndWaitWithResult(() -> { + ConfirmDialog dialog = new ConfirmDialog(parent, "connection_unused_source", + ConfirmDialog.OK_CANCEL_OPTION, false, finalArgument) { + @Override + protected JButton makeOkButton() { + return makeOkButton("connection.save_anyway"); + } + + @Override + protected JButton makeCancelButton() { + return makeCancelButton("connection.back_to_editing"); + } + }; + dialog.setVisible(true); + return dialog.wasConfirmed(); + }); + } + + /** + * Adds the Sources tab to the tabbed pane + */ + protected void addSourcesTab() { + final JPanel sourcesPanel = getSourcesPanel(); + if (sourcesPanel != null) { + getTabbedPane().addTab(ConnectionI18N.getConnectionGUILabel("sources_panel"), sourcesPanel); + } + } + + /** + * Returns the panel for the first tab "Info" + * + * @return the panel for the first tab + */ + protected JPanel getSourcesPanel() { + return new ConnectionSourcesPanel(getParentDialog(), getConnectionModel()); + } + + /** + * Updates the injected parameters + * + * @param parameterModels + * the parameter models + */ + private void setInjected(List parameterModels) { + for (ConnectionParameterModel parameter : parameterModels) { + ConnectionParameterModel parameterModel = getConnectionModel().getParameter(parameter.getGroupName(), parameter.getName()); + if (parameterModel != null) { + parameterModel.setInjectorName(parameter.getInjectorName()); + } + } + } + + /** + * Wraps the given input component in a panel and adds an information icon with a tooltip. The tooltip i18n is + * derived from the type and the parameter. + * + * @param parameterInputComponent + * the component to wrap + * @param type + * the type of the connection the parameter belongs to + * @param parameter + * the connection parameter + * @param parent + * the parent dialog + * @return a new panel containing the old and an additional information icon with tooltip + * @see ConnectionI18N#getParameterTooltip(String, String, String, String) + */ + public static JPanel addInformationIcon(JComponent parameterInputComponent, String type, + ConnectionParameterModel parameter, JDialog parent) { + return addInformationIcon(parameterInputComponent, type, parameter.getGroupName(), parameter.getName(), parent); + } + + /** + * Wraps the given input component in a panel and adds an information icon with a tooltip. The tooltip i18n is + * derived from the type, group and parameter name as {@code gui.label.connection.parameter.{type}.{group}.{ + * parameterName}.tip} and contains the full key of the parameter. + * + * @param parameterInputComponent + * the component to wrap + * @param type + * the type of the connection the parameter belongs to + * @param group + * the group the parameter belongs to + * @param parameterName + * the name of the parameter + * @param parent + * the parent dialog + * @return a new panel containing the old and an additional information icon with tooltip + * @see ConnectionI18N#getParameterTooltip(String, String, String, String) + */ + public static JPanel addInformationIcon(JComponent parameterInputComponent, String type, String group, + String parameterName, JDialog parent) { + // Similar to com.rapidminer.gui.properties.PropertyPanel#getToolTipText + String name = ConnectionI18N.getParameterName(type, group, parameterName, parameterName); + StringBuilder fullTooltip = new StringBuilder(); + fullTooltip.append("

").append(name).append("

"); + String description = ConnectionI18N.getParameterTooltip(type, group, parameterName, null); + if (description != null) { + fullTooltip.append("

"); + fullTooltip.append("").append(TOOLTIP_DESCRIPTION).append(": "); + fullTooltip.append(CLOSING_TAGS.matcher(description).replaceAll("")); + fullTooltip.append("

"); + } + fullTooltip.append("

"); + fullTooltip.append("").append(TOOLTIP_FULL_KEY).append(": "); + fullTooltip.append(group).append(".").append(parameterName); + fullTooltip.append("

"); + return addInformationIcon(parameterInputComponent, fullTooltip.toString(), parent); + } + + /** + * Wraps the given input component in a panel and adds an information icon with a tooltip with the specified text. + * + * @param parameterInputComponent + * the the component to wrap + * @param toolTipText + * the tooltip text to add + * @param parent + * the parent dialog + * @return a new panel containing the old and an additional information icon with tooltip + */ + public static JPanel addInformationIcon(JComponent parameterInputComponent, String toolTipText, JDialog parent) { + JPanel informationWrapper = new JPanel(new BorderLayout()); + informationWrapper.add(parameterInputComponent, BorderLayout.CENTER); + SwingTools.addTooltipHelpIconToLabel(toolTipText, informationWrapper, parent); + return informationWrapper; + } + +} diff --git a/src/main/java/com/rapidminer/connection/gui/ConnectionCreationDialog.java b/src/main/java/com/rapidminer/connection/gui/ConnectionCreationDialog.java new file mode 100644 index 000000000..661f6d6cf --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/ConnectionCreationDialog.java @@ -0,0 +1,588 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General + * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dialog; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.util.Comparator; +import java.util.Vector; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javax.swing.BorderFactory; +import javax.swing.DefaultListCellRenderer; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.KeyStroke; +import javax.swing.ScrollPaneConstants; +import javax.swing.SwingConstants; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import com.rapidminer.connection.ConnectionHandlerRegistry; +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.ConnectionInformationContainerIOObject; +import com.rapidminer.connection.gui.actions.SaveConnectionAction; +import com.rapidminer.connection.gui.dto.ConnectionInformationHolder; +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.gui.RapidMinerGUI; +import com.rapidminer.gui.look.icons.IconFactory; +import com.rapidminer.gui.tools.ExtendedJScrollPane; +import com.rapidminer.gui.tools.ProgressThread; +import com.rapidminer.gui.tools.ResourceAction; +import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.repository.Folder; +import com.rapidminer.repository.MalformedRepositoryLocationException; +import com.rapidminer.repository.Repository; +import com.rapidminer.repository.RepositoryException; +import com.rapidminer.repository.RepositoryLocation; +import com.rapidminer.repository.RepositoryManager; +import com.rapidminer.repository.RepositoryTools; +import com.rapidminer.repository.local.LocalRepository; +import com.rapidminer.tools.I18N; +import com.rapidminer.tools.LogService; +import com.rapidminer.tools.SystemInfoUtilities; +import com.rapidminer.tools.usagestats.ActionStatisticsCollector; + + +/** + * Dialog when a new connection should be created. + * + * @author Marco Boeck + * @since 9.3.0 + */ +public class ConnectionCreationDialog extends JDialog { + + /** + * Constants for temporary hack that allows to start with drivers tab when creating new database connection + * Are the same as constants in com.rapidminer.extension.jdbc.connection.JDBCConnectionHandler. + */ + private static final String JDBC_CONNECTORS_JDBC = "jdbc_connectors:jdbc"; + + private Callable ciCreator; + private Runnable finalAction; + + private enum Status { + NO_STATUS, + + INFO, + + WORKING, + + WARNING + } + + private static final String I18N_KEY = "connection.create_new_connection"; + private static final ImageIcon WARNING_ICON = SwingTools.createIcon("16/" + I18N.getGUILabel("connection.warning.icon")); + private static final ImageIcon INFORMATION_ICON = SwingTools.createIcon("16/" + I18N.getGUILabel("connection.information.icon")); + private static final ImageIcon WORKING_ICON = SwingTools.createIcon("16/" + I18N.getGUILabel("connection.working.icon")); + + private JTextField nameField; + private JComboBox typeBox; + private JComboBox repositoryBox; + private JLabel nameErrorLabel; + private JLabel typeErrorLabel; + private JLabel repositoryErrorLabel; + private JLabel statusIcon; + private JTextArea statusLabel; + private JButton nextButton; + private AtomicBoolean cancelled; + + + /** + * Creates a new dialog instance. + * + * @param parent + * the parent, can be {@code null} + * @param repository + * the repository which should be preselected; can be {@code null} + */ + public ConnectionCreationDialog(Window parent, Repository repository) { + super(parent, I18N.getGUIMessage("gui.dialog." + I18N_KEY + ".title"), Dialog.ModalityType.APPLICATION_MODAL); + + this.cancelled = new AtomicBoolean(false); + + JPanel mainPanel = new JPanel(new BorderLayout()); + + JPanel configPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + + + // info label + JPanel infoPanel = new JPanel(new GridBagLayout()); + gbc.insets = new Insets(20, 20, 5, 20); + gbc.weightx = 1.0; + gbc.fill = GridBagConstraints.HORIZONTAL; + infoPanel.add(new JLabel(I18N.getGUILabel("connection.create_new.info.label")), gbc); + mainPanel.add(infoPanel, BorderLayout.NORTH); + + gbc.weightx = 0; + gbc.gridx = 0; + gbc.gridy = 0; + // connection type chooser + gbc.gridy += 1; + gbc.insets = new Insets(20, 20, 5, 20); + configPanel.add(new JLabel(I18N.getGUILabel("connection.create_new.type.label")), gbc); + + Vector types = new Vector<>(ConnectionHandlerRegistry.getInstance().getAllTypes()); + types.sort(Comparator.comparing(ConnectionI18N::getTypeName)); + typeBox = new JComboBox<>(types); + typeBox.setSelectedIndex(Math.max(0, types.indexOf(JDBC_CONNECTORS_JDBC))); + typeBox.setRenderer(new ConnectionListCellRenderer()); + gbc.gridx += 1; + configPanel.add(typeBox, gbc); + + typeErrorLabel = new JLabel(IconFactory.getEmptyIcon16x16()); + typeErrorLabel.setHorizontalAlignment(SwingConstants.LEFT); + typeErrorLabel.setIconTextGap(10); + gbc.gridy += 1; + gbc.insets = new Insets(0, 20, 5, 20); + configPanel.add(typeErrorLabel, gbc); + + // repository chooser + gbc.gridx = 0; + gbc.gridy += 1; + gbc.insets = new Insets(0, 20, 5, 20); + configPanel.add(new JLabel(I18N.getGUILabel("connection.create_new.repository.label")), gbc); + + Vector repos = RepositoryManager.getInstance(null).getRepositories().stream(). + filter(Repository::supportsConnections). + filter(repo -> !repo.isReadOnly()). + sorted(Comparator.comparing(Repository::getName)). + collect(Collectors.toCollection(Vector::new)); + repositoryBox = new JComboBox<>(repos); + if (repository != null) { + repositoryBox.setSelectedItem(repository); + } + repositoryBox.setRenderer(new DefaultListCellRenderer() { + + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + Repository repo = ((Repository) value); + String name = repo.getName(); + String iconName = repo.getIconName(); + label.setText(name); + label.setIcon(SwingTools.createIcon("16/" + iconName)); + return label; + } + }); + gbc.gridx += 1; + configPanel.add(repositoryBox, gbc); + + repositoryErrorLabel = new JLabel(IconFactory.getEmptyIcon16x16()); + repositoryErrorLabel.setHorizontalAlignment(SwingConstants.LEFT); + repositoryErrorLabel.setIconTextGap(10); + gbc.gridy += 1; + gbc.insets = new Insets(0, 20, 5, 20); + configPanel.add(repositoryErrorLabel, gbc); + + // name field + gbc.gridx = 0; + gbc.gridy += 1; + gbc.insets = new Insets(0, 20, 5, 20); + gbc.weightx = 1.0; + gbc.fill = GridBagConstraints.HORIZONTAL; + configPanel.add(new JLabel(I18N.getGUILabel("connection.create_new.name.label")), gbc); + + nameField = new JTextField(25); + nameField.setToolTipText(I18N.getGUILabel("connection.create_new.name.tip")); + nameField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + nextButton.setEnabled(!validateFieldsAndReturnError()); + } + + @Override + public void removeUpdate(DocumentEvent e) { + nextButton.setEnabled(!validateFieldsAndReturnError()); + } + + @Override + public void changedUpdate(DocumentEvent e) { + nextButton.setEnabled(!validateFieldsAndReturnError()); + } + }); + SwingTools.setPrompt(I18N.getGUILabel("connection.create_new.name.prompt"), nameField); + gbc.gridx += 1; + configPanel.add(nameField, gbc); + + nameErrorLabel = new JLabel(IconFactory.getEmptyIcon16x16()); + nameErrorLabel.setHorizontalAlignment(SwingConstants.LEFT); + nameErrorLabel.setIconTextGap(10); + gbc.gridy += 1; + gbc.insets = new Insets(0, 20, 5, 20); + configPanel.add(nameErrorLabel, gbc); + + // spacer + gbc.gridx = 0; + gbc.gridy += 1; + gbc.weighty = 1.0; + gbc.fill = GridBagConstraints.BOTH; + configPanel.add(new JLabel(), gbc); + + // button panel at bottom + JPanel buttonPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbcb = new GridBagConstraints(); + gbcb.gridx = 0; + gbcb.gridy = 1; + gbcb.weightx = 1; + gbcb.fill = GridBagConstraints.HORIZONTAL; + gbcb.insets = new Insets(10, 20, 10, 10); + + // Status + GridBagConstraints gbcFullWidth = new GridBagConstraints(); + gbcFullWidth.fill = GridBagConstraints.BOTH; + gbcFullWidth.weightx = 1; + gbcFullWidth.gridwidth = GridBagConstraints.REMAINDER; + gbcFullWidth.insets = (Insets) gbcb.insets.clone(); + gbcFullWidth.insets.bottom = 0; + JPanel warningPanel = new JPanel(new GridBagLayout()); + + statusIcon = new JLabel(IconFactory.getEmptyIcon16x16()); + statusLabel = new JTextArea(2, 20); + statusLabel.setMinimumSize(new Dimension(20, 40)); + statusLabel.setBackground(null); + statusLabel.setLineWrap(true); + statusLabel.setWrapStyleWord(true); + statusLabel.setBorder(BorderFactory.createEmptyBorder()); + statusLabel.setEditable(false); + GridBagConstraints gbcStatus = new GridBagConstraints(); + gbcStatus.anchor = GridBagConstraints.NORTH; + warningPanel.add(statusIcon, gbcStatus); + JScrollPane scrollPane = new ExtendedJScrollPane(statusLabel); + scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + scrollPane.setBorder(BorderFactory.createEmptyBorder()); + scrollPane.setPreferredSize(new Dimension(1, 48)); + gbcStatus.fill = GridBagConstraints.HORIZONTAL; + gbcStatus.insets.left = 10; + gbcStatus.weightx = 1.0; + warningPanel.add(scrollPane, gbcStatus); + buttonPanel.add(warningPanel, gbcFullWidth); + buttonPanel.add(new JLabel(), gbcb); + + final ResourceAction nextAction = new ResourceAction("connection.create_new.create") { + @Override + public void loggedActionPerformed(ActionEvent e) { + if (validateFieldsAndReturnError()) { + return; + } + + nextButton.setEnabled(false); + updateStatus(Status.WORKING, I18N.getGUILabel("connection.create_new.status.working")); + + String name = nameField.getText(); + Repository repo = (Repository) repositoryBox.getSelectedItem(); + String locationName = RepositoryLocation.REPOSITORY_PREFIX + repo.getName() + RepositoryLocation.SEPARATOR + + Folder.CONNECTION_FOLDER_NAME + RepositoryLocation.SEPARATOR + name; + + new ProgressThread(SaveConnectionAction.PROGRESS_THREAD_ID_PREFIX, false, locationName) { + @Override + public void run() { + // validation made sure these fields are all set correctly, so nothing is null or empty + String type = String.valueOf(typeBox.getSelectedItem()); + Folder connectionFolder; + RepositoryLocation location; + + try { + connectionFolder = RepositoryTools.getConnectionFolder(repo); + } catch (RepositoryException e1) { + // should not happen, but you never know + LogService.getRoot().log(Level.SEVERE, "com.rapidminer.connection.gui.ConnectionCreationDialog.repo_conn_folder_retrieval", e); + SwingTools.invokeLater(() -> { + nextButton.setEnabled(true); + updateStatus(Status.WARNING, I18N.getGUILabel("connection.create_new.error.repo_conn_folder_retrieval")); + }); + return; + } + + // check if user cancelled dialog in the meantime + if (ConnectionCreationDialog.this.cancelled.get()) { + return; + } + + if (connectionFolder == null) { + // should not happen, but you never know + SwingTools.invokeLater(() -> { + nextButton.setEnabled(true); + updateStatus(Status.WARNING, I18N.getGUILabel("connection.create_new.error.repo_no_conn_folder")); + }); + return; + } + try { + location = new RepositoryLocation(connectionFolder.getLocation(), name); + } catch (MalformedRepositoryLocationException e1) { + // should not happen as it is validated before, but you never know + SwingTools.invokeLater(() -> { + nextButton.setEnabled(true); + nameErrorLabel.setText(e1.getMessage()); + nameErrorLabel.setIcon(WARNING_ICON); + updateStatus(Status.NO_STATUS, null); + }); + return; + } + + // check if user cancelled dialog in the meantime + if (ConnectionCreationDialog.this.cancelled.get()) { + return; + } + + try { + // now we need to check for duplicates. This is a problem on Windows for the Local Repository + // as it will NOT find different capitalization, while on file-system level, it is a duplicate + boolean duplicate; + if (repo instanceof LocalRepository && SystemInfoUtilities.getOperatingSystem() == SystemInfoUtilities.OperatingSystem.WINDOWS) { + duplicate = connectionFolder.getDataEntries().stream().anyMatch(entry -> name.equalsIgnoreCase(entry.getName())); + } else { + // on Unix systems, we can just call locate it it will be fine + duplicate = location.locateEntry() != null; + } + if (duplicate) { + SwingTools.invokeLater(() -> { + nextButton.setEnabled(true); + nameErrorLabel.setText(I18N.getGUILabel("connection.create_new.error.name_duplicate")); + nameErrorLabel.setIcon(INFORMATION_ICON); + updateStatus(Status.NO_STATUS, null); + }); + return; + } + + // check if user cancelled dialog in the meantime + if (ConnectionCreationDialog.this.cancelled.get()) { + return; + } + // convert old connection type if necessary + ConnectionInformation connection = null; + if (ciCreator != null) { + try { + connection = ciCreator.call(); + } catch (Exception e) { + // conversion failed + SwingTools.invokeLater(() -> { + nextButton.setEnabled(true); + nameErrorLabel.setText(I18N.getGUILabel("connection.create_new.error.conversion", e.getMessage())); + nameErrorLabel.setIcon(WARNING_ICON); + updateStatus(Status.NO_STATUS, null); + }); + return; + } + } + + // check if user cancelled dialog in the meantime + if (ConnectionCreationDialog.this.cancelled.get()) { + return; + } + + ConnectionInformationHolder connHolder = connection == null ? + ConnectionInformationHolder.createNewConnection(name, type, location) : + ConnectionInformationHolder.from(connection, name, location); + RepositoryManager.getInstance(null).store(new ConnectionInformationContainerIOObject(connHolder.getConnectionInformation()), location, null); + SwingTools.invokeLater(() -> { + ConnectionCreationDialog.this.dispose(); + if (finalAction != null) { + finalAction.run(); + } + ConnectionEditDialog connectionEditDialog = new ConnectionEditDialog(parent, connHolder, true); + // open up the first relevant tab if possible + connectionEditDialog.showTab(1); + + connectionEditDialog.setVisible(true); + // scroll to connection location + // twice because otherwise the repository browser selects the parent... + RapidMinerGUI.getMainFrame().getRepositoryBrowser().getRepositoryTree().expandAndSelectIfExists(location); + RapidMinerGUI.getMainFrame().getRepositoryBrowser().getRepositoryTree().expandAndSelectIfExists(location); + + }); + } catch (Exception e) { + LogService.getRoot().log(Level.SEVERE, "com.rapidminer.connection.gui.ConnectionCreationDialog.creation_failed", e); + SwingTools.invokeLater(() -> { + nextButton.setEnabled(true); + updateStatus(Status.WARNING, I18N.getGUILabel("connection.create_new.status.failed", e.getMessage())); + }); + } + } + }.start(); + } + }; + gbcb.weightx = 0; + gbcb.insets = new Insets(10, 0, 10, 10); + gbcb.gridx += 1; + nextButton = new JButton(nextAction); + buttonPanel.add(nextButton, gbcb); + + final ResourceAction cancelAction = new ResourceAction("connection.create_new.cancel") { + @Override + public void loggedActionPerformed(ActionEvent e) { + cancelled.set(true); + dispose(); + } + }; + gbcb.gridx += 1; + buttonPanel.add(new JButton(cancelAction), gbcb); + + mainPanel.add(configPanel, BorderLayout.CENTER); + mainPanel.add(buttonPanel, BorderLayout.SOUTH); + + // close dialog with ESC + getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) + .put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false), "CLOSE"); + getRootPane().getActionMap().put("CLOSE", cancelAction); + // next press with ENTER + getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) + .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "NEXT"); + getRootPane().getActionMap().put("NEXT", nextAction); + + setContentPane(mainPanel); + setSize(new Dimension(600, 400)); + setLocationRelativeTo(getOwner()); + + ActionStatisticsCollector.getInstance().log(ActionStatisticsCollector.TYPE_DIALOG, I18N_KEY, "open"); + } + + /** + * Prefills the type and name and locks the type. + * This is used when converting an existing connection. + * + * @param type + * the type of the connection (to be locked in) + * @param name + * the probable name of the connection after conversion + * @see com.rapidminer.connection.legacy.ConversionService ConversionService + */ + public void preFill(String type, String name) { + typeBox.setSelectedItem(type); + typeBox.setEnabled(false); + nameField.setText(name); + } + + /** + * Sets the conversion callable. This should be used when converting an existing connection to convert + * the old connection to a {@link ConnectionInformation}. + * + * @param ciCreator + * the creator/converter; if {@code null}, a new connection will be created + * @see com.rapidminer.connection.legacy.ConversionService ConversionService + */ + public void setConverter(Callable ciCreator) { + this.ciCreator = ciCreator; + } + + /** + * Sets action to be done if the creation was successful. + * This should be used when converting an existing connection, e.g. to close another parent dialog. + * + * @param finalAction + * an action + * @see com.rapidminer.connection.legacy.ConversionService ConversionService + */ + public void setFinalAction(Runnable finalAction) { + this.finalAction = finalAction; + } + + /** + * Updates the status message field. Make sure to call on the EDT. + * + * @param status + * the status, changes the displayed icon. If {@code Status#NO_STATUS}, no icon will be displayed. + * @param message + * the message to display, can be {@code null} + */ + private void updateStatus(Status status, String message) { + switch (status) { + case NO_STATUS: + statusIcon.setIcon(IconFactory.getEmptyIcon16x16()); + break; + case INFO: + statusIcon.setIcon(INFORMATION_ICON); + break; + case WORKING: + statusIcon.setIcon(WORKING_ICON); + break; + case WARNING: + statusIcon.setIcon(WARNING_ICON); + break; + } + statusLabel.setText(message); + statusLabel.setToolTipText(message); + statusIcon.revalidate(); + } + + /** + * Validates the input fields and highlights potential errors. + * + * @return {@code true} if at least one error is contained in the config; {@code false} otherwise + */ + private boolean validateFieldsAndReturnError() { + boolean error = false; + String name = nameField.getText(); + if (name == null || name.trim().isEmpty()) { + nameErrorLabel.setText(I18N.getGUILabel("connection.create_new.error.name_empty")); + nameErrorLabel.setIcon(INFORMATION_ICON); + error = true; + } else if (!RepositoryLocation.isNameValid(name)) { + nameErrorLabel.setText(I18N.getMessage(I18N.getGUIBundle(), + "gui.dialog.repository_location.location_invalid_char.label", RepositoryLocation.getIllegalCharacterInName(name))); + nameErrorLabel.setIcon(WARNING_ICON); + error = true; + } else { + nameErrorLabel.setText(""); + nameErrorLabel.setIcon(IconFactory.getEmptyIcon16x16()); + } + String type = (String) typeBox.getSelectedItem(); + if (type == null) { + typeErrorLabel.setText(I18N.getGUILabel("connection.create_new.error.type_empty")); + typeErrorLabel.setIcon(INFORMATION_ICON); + error = true; + } else { + typeErrorLabel.setText(""); + typeErrorLabel.setIcon(IconFactory.getEmptyIcon16x16()); + } + Repository repo = (Repository) repositoryBox.getSelectedItem(); + if (repo == null) { + repositoryErrorLabel.setText(I18N.getGUILabel("connection.create_new.error.repository_empty")); + repositoryErrorLabel.setIcon(INFORMATION_ICON); + error = true; + } else { + repositoryErrorLabel.setText(""); + repositoryErrorLabel.setIcon(IconFactory.getEmptyIcon16x16()); + } + + return error; + } + +} diff --git a/src/main/java/com/rapidminer/connection/gui/ConnectionEditDialog.java b/src/main/java/com/rapidminer/connection/gui/ConnectionEditDialog.java new file mode 100644 index 000000000..bb6a6dc6c --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/ConnectionEditDialog.java @@ -0,0 +1,293 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui; + +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.Window; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.logging.Level; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JTabbedPane; +import javax.swing.KeyStroke; +import javax.swing.SwingUtilities; + +import com.rapidminer.connection.ConnectionHandlerRegistry; +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.gui.actions.CancelEditingAction; +import com.rapidminer.connection.gui.actions.OpenEditConnectionAction; +import com.rapidminer.connection.gui.actions.SaveConnectionAction; +import com.rapidminer.connection.gui.components.TestConnectionPanel; +import com.rapidminer.connection.gui.dto.ConnectionInformationHolder; +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.connection.util.TestResult; +import com.rapidminer.gui.ApplicationFrame; +import com.rapidminer.repository.RepositoryLocation; +import com.rapidminer.tools.LogService; + + +/** + * The actual dialog to view/edit connections in. To provide the necessary custom UI to edit your connection type, register it at {@link ConnectionGUIRegistry}. + * + * @author Jonas Wilms-Pfau + * @since 9.3.0 + */ +public class ConnectionEditDialog extends JDialog { + + private static final Dimension DEFAULT_SIZE = new Dimension(790, 500); + + private final transient ConnectionInformationHolder holder; + private final transient ConnectionGUI gui; + + private final boolean editMode; + private final TestConnectionPanel testConnectionPanel; + + private final boolean isTypeKnown; + + // the mainGUI will be the ConnectionGUI implementation + private JComponent mainGUI; + + /** + * Opens the dialog in edit mode + * + * @param holder + * the connection holder + */ + public ConnectionEditDialog(ConnectionInformationHolder holder) { + this(holder, true); + } + + /** + * Opens the dialog in view or edit mode + * + * @param holder + * the connection holder + * @param openInEditMode + * {@code true} for edit mode, {@code false} for view mode + */ + public ConnectionEditDialog(ConnectionInformationHolder holder, boolean openInEditMode) { + this(ApplicationFrame.getApplicationFrame(), holder, openInEditMode); + } + + /** + * Opens the dialog in view or edit mode + * + * @param owner + * the owner + * @param holder + * the connection holder + * @param openInEditMode + * {@code true} for edit mode, {@code false} for view mode + */ + public ConnectionEditDialog(Window owner, ConnectionInformationHolder holder, boolean openInEditMode) { + super(owner, getTitle(holder, openInEditMode), ModalityType.APPLICATION_MODAL); + this.isTypeKnown = ConnectionHandlerRegistry.getInstance().isTypeKnown(holder.getConnectionType()); + this.editMode = openInEditMode && holder.isEditable() && isTypeKnown; + this.holder = holder; + this.gui = getEditGui(holder); + this.setLayout(new GridBagLayout()); + setMinimumSize(DEFAULT_SIZE); + setPreferredSize(DEFAULT_SIZE); + + + GridBagConstraints gbc = new GridBagConstraints(); + //----------BIG PANEL------------ + // Set injected Fields + // [Test connection] [Create] [Cancel] + gbc.weightx = 1; + gbc.weighty = 1; + gbc.gridwidth = 3; + gbc.anchor = GridBagConstraints.LINE_START; + gbc.fill = GridBagConstraints.BOTH; + gbc.insets = new Insets(10, 10, 0, 10); + mainGUI = createMainGUI(); + add(mainGUI, gbc); + + gbc.insets = new Insets(0, 0, 0, 0); + gbc.gridy = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weighty = 0; + gbc.gridwidth = 1; + gbc.insets = new Insets(18, 10, 10, 74); + testConnectionPanel = new TestConnectionPanel(holder.getConnectionType(), gui::getConnection, this::processTestResult, holder.getConnectionInformation().getStatistics()); + add(testConnectionPanel, gbc); + + gbc.insets = new Insets(0, 0, 10, 10); + // Give save full with to make alignable to the east + gbc.weightx = 0; + gbc.anchor = GridBagConstraints.SOUTHEAST; + if (!openInEditMode) { + JButton editButton = new JButton(new OpenEditConnectionAction(this, holder)); + editButton.setEnabled(holder.isEditable() && isTypeKnown); + add(editButton, gbc); + } else if (editMode) { + add(new JButton(new SaveConnectionAction(this, gui, holder::getLocation, this::processTestResult, "save_connection")), gbc); + } + gbc.insets = new Insets(0, 0, 10, 10); + CancelEditingAction cancelAction = new CancelEditingAction(this, editMode ? "cancel_connection_edit" : "close_connection_edit", () -> editMode && holder.hasChanged(gui.getConnection())); + add(new JButton(cancelAction), gbc); + + // check changes on close + setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + cancelAction.actionPerformed(null); + } + + }); + // close dialog with ESC + getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) + .put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false), "CLOSE"); + getRootPane().getActionMap().put("CLOSE", cancelAction); + + setLocationRelativeTo(getOwner()); + SwingUtilities.invokeLater(() -> { + pack(); + revalidate(); + repaint(); + }); + } + + /** + * Show a specific tab, to be found via the title. The title is already present resolved from I18N so the result + * from {@link ConnectionI18N#getConnectionGUILabel(String, Object...)} is required here. Fails quietly. + * + * @param name + * value of the I18N key for connection GUI label + */ + public void showTab(String name) { + if (name == null || !(mainGUI instanceof JTabbedPane)) { + return; + } + + JTabbedPane tabPane = (JTabbedPane) mainGUI; + for (int i = 0; i < tabPane.getTabCount(); i++) { + if (name.equals(tabPane.getTitleAt(i))) { + tabPane.setSelectedIndex(i); + break; + } + } + } + + /** + * Show a specific tab, to be found via the index. Fails quietly. + * + * @param index + * the index of the tab to be selected + */ + public void showTab(int index) { + JTabbedPane tabPane = (JTabbedPane) mainGUI; + if (index < 0 || index >= tabPane.getTabCount()) { + return; + } + tabPane.setSelectedIndex(index); + } + + /** + * Get the name of the currently displayed tab or {@code null} + * + * @return the localized title of the currently displayed tab + */ + public String getCurrentTabTitle() { + if (mainGUI instanceof JTabbedPane) { + return ((JTabbedPane) mainGUI).getTitleAt(((JTabbedPane) mainGUI).getSelectedIndex()); + } + return null; + } + + /** + * @return {@code true} if the dialog is in edit mode and the connection is editable + */ + protected boolean isEditable() { + return editMode && holder.isEditable() && isTypeKnown; + } + + private void processTestResult(TestResult testResult) { + testConnectionPanel.setTestResult(testResult); + if (testResult != null && testResult.getType() != TestResult.ResultType.NONE) { + gui.validationResult(testResult.getParameterErrorMessages()); + } + } + + /** + * Creates the main GUI panel. + */ + private JComponent createMainGUI() { + return gui.getConnectionEditComponent(); + } + + /** + * Gets the edit view for the given connection + * + * @param connectionHolder + * the connection + * @return the edit view + */ + private ConnectionGUI getEditGui(ConnectionInformationHolder connectionHolder) { + + ConnectionInformation connection = connectionHolder.getConnectionInformation(); + RepositoryLocation location = connectionHolder.getLocation(); + ConnectionGUI editGui = null; + boolean editable = isEditable(); + + if (!isTypeKnown) { + return new UnknownConnectionTypeGUIProvider().edit(this, connection, location, false); + } + + ConnectionGUIProvider guiProvider = ConnectionGUIRegistry.INSTANCE.getGUIProvider(connection.getConfiguration().getType()); + + if (guiProvider != null) { + editGui = guiProvider.edit(this, connection, location, editable); + } + + // fallback for when connection BEs are developed, and there is no frontend yet. This allows to at least have label - field pairs for each parameter + if (editGui == null) { + LogService.getRoot().log(Level.SEVERE, "com.rapidminer.connection.gui.ConnectionEditDialog.fallback_gui_used", ConnectionI18N.getTypeName(connection.getConfiguration().getType())); + editGui = new DefaultConnectionGUIProvider().edit(this, connection, location, editable); + } + return editGui; + } + + /** + * Returns the title for the given connection information + * + * @param holder + * the connection information + * @param editMode + * if the dialog is opened in edit mode + * @return the dialog title + */ + private static String getTitle(ConnectionInformationHolder holder, boolean editMode) { + final String i18n; + if (holder.isEditable() && editMode && ConnectionHandlerRegistry.getInstance().isTypeKnown(holder.getConnectionType())) { + i18n = "edit_connection"; + } else { + i18n = "view_connection"; + } + return ConnectionI18N.getConnectionGUILabel(i18n, holder.getConnectionInformation().getConfiguration().getName()); + } + +} diff --git a/src/main/java/com/rapidminer/connection/gui/ConnectionGUI.java b/src/main/java/com/rapidminer/connection/gui/ConnectionGUI.java new file mode 100644 index 000000000..bc09f36a7 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/ConnectionGUI.java @@ -0,0 +1,91 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui; + +import java.util.List; +import java.util.Map; +import javax.swing.JComponent; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.gui.model.ConnectionModel; +import com.rapidminer.connection.gui.model.ConnectionParameterGroupModel; +import com.rapidminer.connection.gui.model.ConnectionParameterModel; + + +/** + * Root interface for a GUI that allows users to edit a {@link ConnectionInformation}. Every connection type creator + * must register his own UI for editing said connection type, see {@link ConnectionGUIRegistry}. + *

+ * Note: Do not extend this interface directly, but rather extending {@link AbstractConnectionGUI} and + * register that to the registry! + *

+ * + * @author Jonas Wilms-Pfau, Marco Boeck + * @since 9.3 + */ +public interface ConnectionGUI { + + /** + * This returns a component that is used to edit the entire connection. Only called once during construction time of + * the dialog. + * + * @return the component, never {@code null} + */ + JComponent getConnectionEditComponent(); + + /** + * Returns the current state of the {@link ConnectionInformation}, based on the changes in the GUI the user did. + * Unless the user hits save, this object is not persisted anywhere, e.g. pressing cancel on the dialog will discard + * this. + * + * @return the configured configuration. + * @see com.rapidminer.connection.gui.model.ConnectionModelConverter#applyConnectionModel(ConnectionInformation, + * ConnectionModel) Converter between model and CI + */ + ConnectionInformation getConnection(); + + /** + * Returns the injectable parameters for the given group. + * + * @param group + * the group that is displayed + * @return the injectable parameters for the group + */ + List getInjectableParameters(ConnectionParameterGroupModel group); + + /** + * Called when the validation (see {@link com.rapidminer.connection.ConnectionHandler#validate(Object)}) of the + * current connection has happened. This should be used to indicate the fields that failed validation to the user, to help + * fix the problems. + * + * @param parameterErrorMap + * the map that contains the validation errors. Format: {@code group.key - errorI18N}. To get the human readable, + * i18n string for the error, see {@link com.rapidminer.connection.util.ConnectionI18N#getValidationErrorMessage(String, + * String, String, String)}. + */ + void validationResult(Map parameterErrorMap); + + /** + * Hook for additional checks before saving a connection. If this returns {@code false}, the saving is aborted. Can + * be used to display dialogs with warnings. + * + * @return whether to go on with the saving process + */ + boolean preSaveCheck(); +} diff --git a/src/main/java/com/rapidminer/connection/gui/ConnectionGUIProvider.java b/src/main/java/com/rapidminer/connection/gui/ConnectionGUIProvider.java new file mode 100644 index 000000000..3a050249c --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/ConnectionGUIProvider.java @@ -0,0 +1,50 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui; + +import java.awt.Window; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.repository.RepositoryLocation; + + +/** + * A {@link ConnectionGUIProvider} that contains a method for editing a {@link ConnectionInformation}. + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public interface ConnectionGUIProvider { + + /** + * Creates an edit view for the given configuration. + * + * @param parent + * the parent window + * @param connection + * the connection to edit + * @param location + * the repository location + * @param editable + * if the connection is editable + * @return the gui for the connection configuration + */ + AbstractConnectionGUI edit(Window parent, ConnectionInformation connection, RepositoryLocation location, boolean editable); + +} diff --git a/src/main/java/com/rapidminer/connection/gui/ConnectionGUIRegistry.java b/src/main/java/com/rapidminer/connection/gui/ConnectionGUIRegistry.java new file mode 100644 index 000000000..dfc942c31 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/ConnectionGUIRegistry.java @@ -0,0 +1,84 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui; + +import java.util.HashMap; +import java.util.Map; + +import com.rapidminer.tools.ValidationUtil; +import com.rapidminer.tools.Tools; + + +/** + * Registry for Connection GUIs. Every connection type must register his editing GUI here. + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public enum ConnectionGUIRegistry { + + INSTANCE; + + private final Map providers = new HashMap<>(); + + + /** + * Registers a provider for a connection type + * + * @param provider + * the provider for the type + * @param connectionType + * the connection type + * @return {@code false} if another provider is already registered for the connectionType + */ + public boolean registerGUIProvider(ConnectionGUIProvider provider, String connectionType) { + ValidationUtil.requireNonNull(provider, "provider"); + ValidationUtil.requireNonNull(connectionType, "connectionType"); + return null == providers.putIfAbsent(connectionType, provider); + } + + /** + * Unregisters a provider for a connection type + * + * @param provider + * the provider for the type + * @param connectionType + * the connection type + * @return {@code true} if the handler was successfully unregistered + */ + public boolean unregisterGUIProvider(ConnectionGUIProvider provider, String connectionType) { + ValidationUtil.requireNonNull(provider, "provider"); + ValidationUtil.requireNonNull(connectionType, "connectionType"); + return providers.remove(connectionType, provider); + } + + /** + * Returns the ConnectionGUIProvider for the given type or {@code null} if none is set + *

Internal API, only available to signed Extensions.

+ * @param connectionType + * the connection type + * @return the registered {@link ConnectionGUIProvider} for the type + * @throws UnsupportedOperationException if the caller is not signed + */ + public ConnectionGUIProvider getGUIProvider(String connectionType) { + Tools.requireInternalPermission(); + return providers.get(connectionType); + } + +} diff --git a/src/main/java/com/rapidminer/connection/gui/ConnectionInformationRenderer.java b/src/main/java/com/rapidminer/connection/gui/ConnectionInformationRenderer.java new file mode 100644 index 000000000..cd9b5289c --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/ConnectionInformationRenderer.java @@ -0,0 +1,68 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General + * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui; + +import java.awt.BorderLayout; +import java.awt.Component; +import javax.swing.JPanel; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.ConnectionInformationContainerIOObject; +import com.rapidminer.connection.gui.components.ConnectionInfoPanel; +import com.rapidminer.connection.gui.model.ConnectionModelConverter; +import com.rapidminer.gui.renderer.AbstractRenderer; +import com.rapidminer.operator.IOContainer; +import com.rapidminer.report.Reportable; +import com.rapidminer.repository.Repository; + + +/** + * Renderer for {@link ConnectionInformationContainerIOObject}s that reuses the {@link ConnectionInfoPanel}. + * + * @author Gisa Meier + * @since 9.3.0 + */ +public class ConnectionInformationRenderer extends AbstractRenderer { + + @Override + public Reportable createReportable(Object renderable, IOContainer ioContainer, int desiredWidth, int desiredHeight) { + return null; + } + + @Override + public String getName() { + return "Connection"; + } + + @Override + public Component getVisualizationComponent(Object renderable, IOContainer ioContainer) { + ConnectionInformationContainerIOObject connectionObject = (ConnectionInformationContainerIOObject) renderable; + ConnectionInformation connectionInformation = connectionObject.getConnectionInformation(); + Repository repository = connectionInformation.getRepository(); + + ConnectionInfoPanel connectionInfoPanel = + new ConnectionInfoPanel(ConnectionModelConverter.fromConnection(connectionInformation, + repository != null ? repository.getLocation() : null,false), false); + JPanel panel = new JPanel(new BorderLayout()); + panel.add(connectionInfoPanel, BorderLayout.NORTH); + return panel; + } + + +} diff --git a/src/main/java/com/rapidminer/connection/gui/ConnectionListCellRenderer.java b/src/main/java/com/rapidminer/connection/gui/ConnectionListCellRenderer.java new file mode 100644 index 000000000..761fcf764 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/ConnectionListCellRenderer.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General + * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui; + +import java.awt.Component; +import javax.swing.DefaultListCellRenderer; +import javax.swing.JLabel; +import javax.swing.JList; + +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.gui.tools.IconSize; + + +/** + * Renders connection types in a list. + * + * @author Marco Boeck + * @since 9.3.0 + */ +public class ConnectionListCellRenderer extends DefaultListCellRenderer { + + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + String type = String.valueOf(value); + label.setText(ConnectionI18N.getTypeName(type)); + label.setIcon(ConnectionI18N.getConnectionIcon(type, IconSize.SMALL)); + return label; + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/DefaultConnectionGUI.java b/src/main/java/com/rapidminer/connection/gui/DefaultConnectionGUI.java new file mode 100644 index 000000000..e60ce0c5d --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/DefaultConnectionGUI.java @@ -0,0 +1,119 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui; + +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.Window; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.gui.components.ConnectionParameterLabel; +import com.rapidminer.connection.gui.components.ConnectionParameterTextField; +import com.rapidminer.connection.gui.model.ConnectionModel; +import com.rapidminer.connection.gui.model.ConnectionParameterGroupModel; +import com.rapidminer.connection.gui.model.ConnectionParameterModel; +import com.rapidminer.repository.RepositoryLocation; + + +/** + * Default {@link ConnectionGUI} implementation in case no UI was registered to the {@link ConnectionGUIRegistry}. + * Also can be extended for connection types that only require a simple UI. + * More complex UIs should be implemented by extending {@link AbstractConnectionGUI} directly. + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public class DefaultConnectionGUI extends AbstractConnectionGUI { + + + protected DefaultConnectionGUI(Window parent, ConnectionInformation connection, RepositoryLocation location, boolean editable) { + super(parent, connection, location, editable); + } + + @Override + public JComponent getComponentForGroup(ConnectionParameterGroupModel groupModel, ConnectionModel connectionModel) { + JPanel panel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.weightx = 1.0; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.anchor = GridBagConstraints.NORTHWEST; + gbc.gridx = 0; + gbc.insets = new Insets(15, 32, 0, 32); + JPanel components = new JPanel(new GridBagLayout()); + GridBagConstraints left = new GridBagConstraints(); + GridBagConstraints right = new GridBagConstraints(); + right.weightx = 1; + left.insets = new Insets(0, 0, VERTICAL_COMPONENT_SPACING, HORIZONTAL_COMPONENT_SPACING); + right.fill = left.fill = GridBagConstraints.HORIZONTAL; + right.insets = new Insets(0, 0, VERTICAL_COMPONENT_SPACING, 0); + left.gridx = 0; + right.gridx = 1; + for (ConnectionParameterModel parameter : groupModel.getParameters()) { + components.add(getParameterLabelComponent(connectionModel.getType(), parameter), left); + JComponent parameterInputComponent = getParameterInputComponent(connectionModel.getType(), parameter); + JPanel informationWrapper = addInformationIcon(parameterInputComponent, connectionModel.getType(), + parameter, getParentDialog()); + components.add(informationWrapper, right); + } + panel.add(components, gbc); + + gbc.weighty = 1.0; + gbc.fill = GridBagConstraints.VERTICAL; + // Add empty label to move stuff up + panel.add(new JLabel(), gbc); + + return panel; + } + + /** + * Creates the label component for the given {@link ConnectionParameterModel parameter} and type. + * Subclasses can override this to customize a simple UI. More complex UIs should be implemented by extending + * {@link AbstractConnectionGUI} directly. + * + * @param type + * the type of the connection (see {@link ConnectionModel#getType()}) + * @param parameter + * the parameter + * @return a {@link ConnectionParameterLabel} by default + */ + protected JComponent getParameterLabelComponent(String type, ConnectionParameterModel parameter) { + return new ConnectionParameterLabel(type, parameter); + } + + /** + * Creates the input component for the given {@link ConnectionParameterModel parameter} and type. + * Subclasses can override this to customize a simple UI. If the parameter is injectable, the returned component + * should be wrapped inside a {@link com.rapidminer.connection.gui.components.InjectableComponentWrapper}. + * More complex UIs should be implemented by extending {@link AbstractConnectionGUI} directly. + * + * @param type + * the type of the connection (see {@link ConnectionModel#getType()}) + * @param parameter + * the parameter + * @return a {@link ConnectionParameterTextField} by default + */ + protected JComponent getParameterInputComponent(String type, ConnectionParameterModel parameter) { + return new ConnectionParameterTextField(type, parameter); + } + +} diff --git a/src/main/java/com/rapidminer/connection/gui/DefaultConnectionGUIProvider.java b/src/main/java/com/rapidminer/connection/gui/DefaultConnectionGUIProvider.java new file mode 100644 index 000000000..1d642757a --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/DefaultConnectionGUIProvider.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui; + +import java.awt.Window; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.repository.RepositoryLocation; + + +/** + * Default provider of a {@link ConnectionGUI}. + * + * @author Jonas Wilms-Pfau + * @since 9.3.0 + */ +public class DefaultConnectionGUIProvider implements ConnectionGUIProvider { + + @Override + public AbstractConnectionGUI edit(Window parent, ConnectionInformation connection, RepositoryLocation location, boolean editable) { + return new DefaultConnectionGUI(parent, connection, location, editable); + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/InjectParametersDialog.java b/src/main/java/com/rapidminer/connection/gui/InjectParametersDialog.java new file mode 100644 index 000000000..dd4d4e356 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/InjectParametersDialog.java @@ -0,0 +1,451 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dialog; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ItemEvent; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import javax.swing.BorderFactory; +import javax.swing.DefaultListCellRenderer; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.KeyStroke; +import javax.swing.SwingConstants; +import javax.swing.WindowConstants; + +import org.apache.commons.lang.StringUtils; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.gui.components.ConnectionSourcesPanel; +import com.rapidminer.connection.gui.model.ConnectionModelConverter; +import com.rapidminer.connection.gui.model.ConnectionParameterModel; +import com.rapidminer.connection.gui.model.InjectParametersModel; +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.connection.util.TestResult; +import com.rapidminer.connection.valueprovider.ValueProvider; +import com.rapidminer.connection.valueprovider.handler.ValueProviderHandler; +import com.rapidminer.connection.valueprovider.handler.ValueProviderHandlerRegistry; +import com.rapidminer.gui.look.icons.EmptyIcon; +import com.rapidminer.gui.tools.ExtendedJComboBox; +import com.rapidminer.gui.tools.ExtendedJScrollPane; +import com.rapidminer.gui.tools.FilterListener; +import com.rapidminer.gui.tools.FilterTextField; +import com.rapidminer.gui.tools.ResourceAction; +import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.gui.tools.TextFieldWithAction; +import com.rapidminer.tools.I18N; +import com.rapidminer.tools.ValidationUtil; +import com.rapidminer.tools.usagestats.ActionStatisticsCollector; + + +/** + * The InjectParametersDialog shows the dialog to edit injection settings for connection parameters. + * + * @author Andreas Timm + * @since 9.3 + */ +public class InjectParametersDialog extends JDialog { + + // Width for all comboboxes + private static final int COMBOBOX_WIDTH = 297; + // Icons for properly or not properly configured value providers + private static final ImageIcon ICON_WARNING = SwingTools.createIcon("16/" + I18N.getGUIMessage(ConnectionSourcesPanel.NOT_WELL_CONFIGURED_VALUE_PROVIDER_ICON)); + // a large main icon used in the top left corner + private static final ImageIcon LARGE_CONNECTION_ICON = SwingTools.createIcon("48/" + I18N.getGUIMessage("gui.dialog.inject_connection_parameter.icon")); + // Icon and hover-icon to show when using the search and give the ability to clear the search + private static final ImageIcon CLEAR_FILTER_HIGHLIGHT_ICON = SwingTools.createIcon("16/" + I18N.getGUIMessage("gui.action.inject_connection_clear_filter.highlight_icon")); + private static final ImageIcon NO_RESULTS_ICON = SwingTools.createIcon("16/" + I18N.getGUIMessage("gui.dialog.inject_connection_parameter.no_results.icon")); + + // A generic warning panel to display if a malconfigured value provider was chosen + private static final JLabel WARNING_PANEL = new JLabel(I18N.getGUIMessage("gui.dialog.inject_connection.value_provider_configuration_warning"), ICON_WARNING, SwingConstants.LEFT); + // an empty panel to show if there is no warning to be shown + private static final JPanel EMPTY_PANEL = new JPanel(); + // default value provider to use if unknown + private static final int DEFAULT_VALUE_PROVIDER_INDEX = 0; + // empty icon so comboboxes won't jump around + private static final EmptyIcon EMPTY_ICON = new EmptyIcon(0, 16); + + /** + * The data to use + */ + private final InjectParametersModel data; + /** + * The connection type + */ + private final String type; + /** + * Scrollable parameter list, update can be necessary + */ + private JPanel parameterPanel = new JPanel(new GridBagLayout()); + /** + * Content of the search field for filtering the parameters + */ + private String searchValue = ""; + /** + * Init on creation the set of properly configured ValueProviders to use this information to show icons and the + * warning panel. + */ + private Set wellConfiguredVps = new HashSet<>(); + /** + * To be changed upon closing the dialog via OK button, else this default is sufficient + */ + private boolean wasConfirmed = false; + + + /** + * Renderer to show icons for the {@link ValueProvider} configuration state + */ + private static final class ValueProviderRenderer implements javax.swing.ListCellRenderer { + + private final DefaultListCellRenderer renderer = new DefaultListCellRenderer(); + private final ConnectionParameterModel parameter; + private Set wellConfiguredVps; + + private ValueProviderRenderer(ConnectionParameterModel parameter, Set wellConfiguredVps) { + this.parameter = parameter; + this.wellConfiguredVps = wellConfiguredVps; + } + + @Override + public Component getListCellRendererComponent(JList list, ValueProvider vp, int index, boolean isSelected, boolean cellHasFocus) { + renderer.getListCellRendererComponent(list, vp, index, isSelected, cellHasFocus); + ValueProviderGUIProvider guiProvider = ValueProviderGUIRegistry.INSTANCE.getGUIProvider(vp.getType()); + String hint = guiProvider == null ? null : guiProvider.getCustomLabel(ValueProviderGUIProvider.CustomLabel.INJECTOR_SELECTION, vp, ConnectionModelConverter.getConnection(parameter), parameter.getGroupName(), parameter.getName()); + String text = hint == null ? vp.getName() : hint; + renderer.setText(text); + if (wellConfiguredVps.contains(vp.getName())) { + renderer.setIcon(EMPTY_ICON); + renderer.setToolTipText(null); + } else { + renderer.setIcon(ICON_WARNING); + renderer.setToolTipText(I18N.getGUIMessage("gui.dialog.connection.valueprovider.needs_configuration.tip")); + } + return renderer; + } + } + + /** + * Renderer for the empty combobox, automatically producing the correct height + */ + private static final DefaultListCellRenderer EMPTY_DEFAULT_LIST_CELL_RENDERER = new DefaultListCellRenderer() { + + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + this.setText((String) value); + this.setIcon(EMPTY_ICON); + return this; + } + }; + + + /** + * Create a new instance that immediately shows the dialog for the given data as a + * + * @param owner + * a {@link Window} that is the parent for this modal dialog + * @param type + * the {@link com.rapidminer.connection.gui.model.ConnectionModel#getType() connection type} + * @param key + * the i18n key used for the properties gui.dialog.-key-.title and gui.dialog.-key-.icon + * @param data + * the data, containing parameters to be altered and available {@link ValueProvider ValueProviders} + */ + public InjectParametersDialog(Window owner, String type, String key, InjectParametersModel data) { + super(ValidationUtil.requireNonNull(owner, "owner"), I18N.getMessage(I18N.getGUIBundle(), "gui.dialog." + key + ".title"), Dialog.ModalityType.APPLICATION_MODAL); + + ValidationUtil.requireNonNull(data, "data"); + ValidationUtil.requireNonNull(type, "type"); + this.data = data; + this.type = type; + + checkVPConfigurations(); + + FilterTextField tf = new FilterTextField(25); + FilterListener filterListener = filter -> { + searchValue = tf.getText(); + SwingTools.invokeLater(this::fillParameterPanel); + }; + TextFieldWithAction textField = new TextFieldWithAction(tf, new ResourceAction("inject_connection_clear_filter") { + @Override + public void loggedActionPerformed(ActionEvent e) { + tf.setText(""); + filterListener.valueChanged(""); + } + }, CLEAR_FILTER_HIGHLIGHT_ICON); + + tf.setDefaultFilterText(I18N.getGUIMessage("gui.field.inject_parameters.prompt")); + tf.addFilterListener(filterListener); + + ExtendedJScrollPane scrollPane = new ExtendedJScrollPane(parameterPanel); + scrollPane.setBorder(null); + + JPanel buttonPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbcb = new GridBagConstraints(); + gbcb.gridx = 0; + gbcb.gridy = 0; + gbcb.anchor = GridBagConstraints.WEST; + gbcb.fill = GridBagConstraints.HORIZONTAL; + gbcb.insets = new Insets(8, 16, 10, 10); + gbcb.weightx = 1; + buttonPanel.add(WARNING_PANEL, gbcb); + buttonPanel.add(EMPTY_PANEL, gbcb); + + final ResourceAction saveAction = new ResourceAction("connection.save_injection") { + @Override + public void loggedActionPerformed(ActionEvent e) { + wasConfirmed = true; + data.setChangedParameters(); + dispose(); + } + }; + gbcb.weightx = 0; + gbcb.insets.left = 0; + gbcb.gridx += 1; + buttonPanel.add(new JButton(saveAction), gbcb); + final ResourceAction cancelAction = new ResourceAction("connection.cancel_injection_edit") { + @Override + public void loggedActionPerformed(ActionEvent e) { + data.resetChangedParameters(); + dispose(); + } + }; + + gbcb.gridx += 1; + buttonPanel.add(new JButton(cancelAction), gbcb); + + final JPanel topPanel = new JPanel(new BorderLayout()); + final JLabel comp = new JLabel(I18N.getGUIMessage("gui.dialog.inject_connection_parameter.message"), LARGE_CONNECTION_ICON, SwingConstants.LEFT); + topPanel.setBorder(BorderFactory.createEmptyBorder(8, 16, 8, 16)); + comp.setIconTextGap(8); + comp.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0)); + + topPanel.add(comp, BorderLayout.CENTER); + topPanel.add(textField, BorderLayout.SOUTH); + + final JPanel pane = new JPanel(new BorderLayout()); + pane.add(topPanel, BorderLayout.NORTH); + pane.add(scrollPane, BorderLayout.CENTER); + pane.add(buttonPanel, BorderLayout.SOUTH); + setContentPane(pane); + + setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + // check changes on close + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + cancelAction.actionPerformed(null); + } + + }); + // close dialog with ESC + getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) + .put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false), "CLOSE"); + getRootPane().getActionMap().put("CLOSE", cancelAction); + + + fillParameterPanel(); + checkAndShowWarning(); + + ActionStatisticsCollector.getInstance().log(ActionStatisticsCollector.TYPE_INJECT_VALUE_PROVIDER_DIALOG, key, "open"); + pack(); + setLocationRelativeTo(getOwner()); + } + + /** + * Check the available value providers from the data and keep information if they are properly configured + */ + private void checkVPConfigurations() { + ConnectionInformation connection = null; + if (!data.getParameters().isEmpty()) { + connection = ConnectionModelConverter.getConnection(data.getParameters().get(0)); + } + for (ValueProvider vp : data.getValueProviders()) { + if (ValueProviderHandlerRegistry.getInstance().isTypeKnown(vp.getType())) { + final ValueProviderHandler handler = ValueProviderHandlerRegistry.getInstance().getHandler(vp.getType()); + if (handler.validate(vp, connection).getType() == TestResult.ResultType.SUCCESS) { + wellConfiguredVps.add(vp.getName()); + } + } + } + } + + /** + * After instantiation of this class the thread is blocked until the dialog was closed, call this method to receive + * the user input so if the user clicked on the OK button, which would return true. + * + * @return if the user closed the dialog using the OK button {@code true}, else {@code false} + */ + public boolean wasConfirmed() { + return wasConfirmed; + } + + /** + * Set the content of the scrollpane aka parameterPanel. + */ + private void fillParameterPanel() { + parameterPanel.removeAll(); + + final ValueProvider[] valueProviders = data.getValueProviders().toArray(new ValueProvider[0]); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(10, 10, 0, 0); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.anchor = GridBagConstraints.NORTHWEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + List filteredParameters = getFilteredParameters(); + for (ConnectionParameterModel parameter : filteredParameters) { + ExtendedJComboBox selectBox = new ExtendedJComboBox<>(new String[]{I18N.getGUIMessage("gui.dialog.inject_connection.select")}); + selectBox.setEnabled(false); + selectBox.setVisible(!parameter.isInjected()); + selectBox.setPreferredWidth(COMBOBOX_WIDTH); + selectBox.setRenderer(EMPTY_DEFAULT_LIST_CELL_RENDERER); + + ExtendedJComboBox comboBox; + comboBox = new ExtendedJComboBox<>(valueProviders); + if (parameter.getInjectorName() != null) { + Optional optionalValueProvider = Arrays.stream(valueProviders).filter(vp -> vp.getName().equals(parameter.getInjectorName())).findFirst(); + optionalValueProvider.ifPresent(comboBox::setSelectedItem); + } + comboBox.setEnabled(parameter.isEditable()); + comboBox.setVisible(parameter.isInjected()); + comboBox.setPreferredWidth(COMBOBOX_WIDTH); + comboBox.addActionListener(a -> { + final ValueProvider selectedItem = (ValueProvider) comboBox.getSelectedItem(); + if (selectedItem != null) { + parameter.setInjectorName(selectedItem.getName()); + } + checkAndShowWarning(); + }); + comboBox.setRenderer(new ValueProviderRenderer(parameter, wellConfiguredVps)); + + gbc.anchor = GridBagConstraints.WEST; + gbc.gridx = 0; + gbc.weightx = 0; + final JCheckBox checkBox = new JCheckBox(ConnectionI18N.getParameterName(type, parameter.getGroupName(), parameter.getName(), parameter.getName())); + checkBox.setSelected(parameter.isInjected()); + checkBox.setEnabled(parameter.isEditable()); + checkBox.setToolTipText(I18N.getGUILabel("connection.valueprovider.key_for_injection.label", parameter.getName())); + checkBox.addItemListener(e -> { + final boolean injected = e.getStateChange() == ItemEvent.SELECTED; + String name = setParameterInjected(parameter, injected); + selectBox.setVisible(!injected); + if (injected && name != null) { + comboBox.setSelectedIndex(DEFAULT_VALUE_PROVIDER_INDEX); + } + comboBox.setVisible(injected); + checkAndShowWarning(); + }); + parameterPanel.add(checkBox, gbc); + + gbc.anchor = GridBagConstraints.CENTER; + gbc.weightx = 1; + gbc.gridx++; + gbc.fill = GridBagConstraints.HORIZONTAL; + parameterPanel.add(new JLabel(), gbc); + gbc.gridx++; + gbc.weightx = 0; + gbc.fill = GridBagConstraints.NONE; + int oldri = gbc.insets.right; + gbc.insets.right = 16; + parameterPanel.add(comboBox, gbc); + parameterPanel.add(selectBox, gbc); + gbc.insets.right = oldri; + gbc.gridy++; + } + + if (filteredParameters.isEmpty()) { + if (data.getParameters().size() > filteredParameters.size()) { + parameterPanel.add(new JLabel(I18N.getGUIMessage("gui.dialog.inject_connection_parameter.no_results.label"), NO_RESULTS_ICON, SwingConstants.LEFT), gbc); + } else { + parameterPanel.add(new JLabel(I18N.getGUIMessage("gui.dialog.inject_connection_parameter.no_parameters.label"), NO_RESULTS_ICON, SwingConstants.LEFT), gbc); + } + } + + gbc.weighty = 1; + parameterPanel.add(new JLabel(), gbc); + revalidate(); + repaint(); + } + + /** + * Check all the parameters and see if any contains a not properly configured value provider, show the info then + */ + private void checkAndShowWarning() { + boolean showWarning = false; + for (ConnectionParameterModel param : data.getParameters()) { + if (param.getInjectorName() != null && !wellConfiguredVps.contains(param.getInjectorName())) { + showWarning = true; + break; + } + } + WARNING_PANEL.setVisible(showWarning); + EMPTY_PANEL.setVisible(!showWarning); + } + + /** + * Set the value if the given parameter is injected, will directly set the first available {@link ValueProvider} + */ + private String setParameterInjected(ConnectionParameterModel parameter, boolean injected) { + if (!injected) { + parameter.setInjectorName(null); + } else if (!data.getValueProviders().isEmpty()) { + String name = data.getValueProviders().get(DEFAULT_VALUE_PROVIDER_INDEX).getName(); + parameter.setInjectorName(name); + return name; + } + return null; + } + + /** + * Filter the list of parameters and return only that that do contain the searchvalue. + */ + private List getFilteredParameters() { + List list = new ArrayList<>(); + for (ConnectionParameterModel p : data.getParameters()) { + String paramName = ConnectionI18N.getParameterName(p.getType(), p.getGroupName(), p.getName(), p.getName()); + if (StringUtils.containsIgnoreCase(paramName, searchValue)) { + list.add(p); + } + } + return list; + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/UnknownConnectionTypeGUI.java b/src/main/java/com/rapidminer/connection/gui/UnknownConnectionTypeGUI.java new file mode 100644 index 000000000..c99376544 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/UnknownConnectionTypeGUI.java @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui; + +import java.awt.Window; + +import javax.swing.JComponent; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.gui.model.ConnectionModel; +import com.rapidminer.connection.gui.model.ConnectionParameterGroupModel; +import com.rapidminer.repository.RepositoryLocation; + +/** + * Default {@link ConnectionGUI} implementation in case the type was not registered. + * + *

Does only display the info panel. Not editable.

+ * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public class UnknownConnectionTypeGUI extends DefaultConnectionGUI { + + public UnknownConnectionTypeGUI(Window parent, ConnectionInformation connection, RepositoryLocation location, boolean editable) { + super(parent, connection, location, false); + } + + @Override + protected void addSourcesTab() { + // do nothing + } + + @Override + public JComponent getComponentForGroup(ConnectionParameterGroupModel groupModel, ConnectionModel connectionModel) { + return null; + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/UnknownConnectionTypeGUIProvider.java b/src/main/java/com/rapidminer/connection/gui/UnknownConnectionTypeGUIProvider.java new file mode 100644 index 000000000..5c18e93db --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/UnknownConnectionTypeGUIProvider.java @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui; + +import java.awt.Window; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.repository.RepositoryLocation; + +/** + * Default provider of a {@link ConnectionGUI} for unknown connection types. + * + * @author Jonas Wilms-Pfau + * @since 9.3.0 + */ +public class UnknownConnectionTypeGUIProvider implements ConnectionGUIProvider{ + + @Override + public AbstractConnectionGUI edit(Window parent, ConnectionInformation connection, RepositoryLocation location, boolean editable) { + return new UnknownConnectionTypeGUI(parent, connection, location, editable); + } + + +} diff --git a/src/main/java/com/rapidminer/connection/gui/ValueProviderGUIProvider.java b/src/main/java/com/rapidminer/connection/gui/ValueProviderGUIProvider.java new file mode 100644 index 000000000..cef40152e --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/ValueProviderGUIProvider.java @@ -0,0 +1,110 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import javax.swing.JComponent; +import javax.swing.JDialog; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.connection.valueprovider.ValueProvider; +import com.rapidminer.connection.valueprovider.handler.ValueProviderHandler; +import com.rapidminer.tools.I18N; + + +/** + * GUI Provider to configure complicated setups for {@link com.rapidminer.connection.valueprovider.ValueProvider ValueProviders}. These need to take care of setting the values immediately themselves. + * + * @author Andreas Timm + * @since 9.3 + */ +public interface ValueProviderGUIProvider { + + /** + * Used in the {@link #getCustomLabel} method to add more information to the + * injection label and the injector selection. + */ + enum CustomLabel { + /** + * Used as a indicator for injected parameter, default "injected by Injector Name" + * I18N key: "gui.label.connection.valueprovider.type.{valueProviderType}.injected_parameter.label" + */ + INJECTED_PARAMETER, + /** + * Used for the injector selection dropdown, default "Injector Name" + * I18N key: "gui.label.connection.valueprovider.type.{valueProviderType}.injector_selection.label" + */ + INJECTOR_SELECTION + } + + /** + * If this provider was registered it has to handle the complete configuration of this {@link ValueProviderHandler} + * + * @param parent + * the parent dialog + * @param handler + * the handler that should be configured + * @param connection + * the connection which contains the handler + * @param editmode + * if the editmode should be active, so instead of showing the values the user should be able to edit them + * @return a UI component to edit the given handler + */ + JComponent createConfigurationComponent(JDialog parent, ValueProvider handler, ConnectionInformation connection, boolean editmode); + + /** + * Returns a custom label for either the value provider selection, or the placeholder text + *

The default implementation uses the keys

"gui.label.connection.valueprovider.type.{valueProviderType}.injected_parameter.label"
+ * and + *
"gui.label.connection.valueprovider.type.{valueProviderType}.injector_selection.label"

+ *

+ * + * Implementations should just call {@code super.getCustomLabel} with additional i18n arguments. The value provider + * name is always passed as the first (0) parameter, parameter 1 till n are implementation specific. + * + * @param key + * the label type + * @param provider + * the value provider + * @param connection + * the connection which contains the value provider + * @param group + * the group of the parameter which is injected + * @param parameterKey + * the parameter key which is injected + * @param args + * can be used by implementations to pass additional i18n arguments + * @return the custom label, or {@code null} if no custom label exists + */ + default String getCustomLabel(CustomLabel key, ValueProvider provider, ConnectionInformation connection, String group, String parameterKey, Object... args) { + List params = new ArrayList<>(); + params.add(provider.getName()); + if (args != null) { + params.addAll(Arrays.asList(args)); + } + String type = provider.getType().replace(':', '.'); + String keyString = key.toString().toLowerCase(Locale.ENGLISH); + String fullKey = String.join(ConnectionI18N.KEY_DELIMITER, ConnectionI18N.VALUE_PROVIDER_TYPE_PREFIX, type, keyString, ConnectionI18N.LABEL_SUFFIX); + return I18N.getGUIMessageOrNull(fullKey, params.toArray()); + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/ValueProviderGUIRegistry.java b/src/main/java/com/rapidminer/connection/gui/ValueProviderGUIRegistry.java new file mode 100644 index 000000000..94529e340 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/ValueProviderGUIRegistry.java @@ -0,0 +1,102 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui; + +import java.util.HashMap; +import java.util.Map; + +import com.rapidminer.connection.DefaultValueProviderGUI; +import com.rapidminer.connection.valueprovider.handler.MacroValueProviderGUI; +import com.rapidminer.connection.valueprovider.handler.MacroValueProviderHandler; +import com.rapidminer.tools.Tools; +import com.rapidminer.tools.ValidationUtil; + + +/** + * Registry for {@link com.rapidminer.connection.valueprovider.handler.ValueProviderHandler} configuration GUIs. + * If the configuration is more than simple key value combinations it has to register its own provider for configuration + * of complex setups. + * + * @author Andreas Timm + * @since 9.3 + */ +public enum ValueProviderGUIRegistry { + INSTANCE; + + /** + * The default GUI provider to configure the parameters of a {@link com.rapidminer.connection.valueprovider.ValueProvider ValueProvider} + */ + private static final ValueProviderGUIProvider DEFAULT_VP_GUI_PROVIDER = new DefaultValueProviderGUI(); + + ValueProviderGUIRegistry() { + registerGUIProvider(new MacroValueProviderGUI(), MacroValueProviderHandler.TYPE); + } + + + /** + * all the registered {@link ValueProviderGUIProvider ValueProviderGUIProviders} + */ + private final Map providers = new HashMap<>(); + + /** + * Registers a provider for a value provider type + * + * @param provider + * the provider for the type + * @param valueProviderType + * the value provider type + * @return {@code false} if another provider is already registered for the valueProviderType + */ + public boolean registerGUIProvider(ValueProviderGUIProvider provider, String valueProviderType) { + ValidationUtil.requireNonNull(provider, "provider"); + ValidationUtil.requireNonNull(valueProviderType, "valueProviderType"); + return null == providers.putIfAbsent(valueProviderType, provider); + } + + /** + * Unregisters a provider for a value provider type + * + * @param provider + * the provider for the type + * @param valueProviderType + * the value provder type + * @return {@code true} if the handler was successfully unregistered + */ + public boolean unregisterGUIProvider(ValueProviderGUIProvider provider, String valueProviderType) { + ValidationUtil.requireNonNull(provider, "provider"); + ValidationUtil.requireNonNull(valueProviderType, "valueProviderType"); + return providers.remove(valueProviderType, provider); + } + + /** + * Returns the component for the given type or {@code null} if none is set + *

Internal API, only available to signed Extensions.

+ * + * @param valueProviderType + * the value provider type + * @return the registered {@link ValueProviderGUIProvider} for the type or a default renderer, never {@code null} + * @throws UnsupportedOperationException + * if the caller is not signed + */ + public ValueProviderGUIProvider getGUIProvider(String valueProviderType) { + Tools.requireInternalPermission(); + return providers.getOrDefault(valueProviderType, DEFAULT_VP_GUI_PROVIDER); + } + +} diff --git a/src/main/java/com/rapidminer/connection/gui/actions/CancelEditingAction.java b/src/main/java/com/rapidminer/connection/gui/actions/CancelEditingAction.java new file mode 100644 index 000000000..9bd63af5b --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/actions/CancelEditingAction.java @@ -0,0 +1,67 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.actions; + +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.util.function.BooleanSupplier; + +import com.rapidminer.gui.tools.ResourceAction; +import com.rapidminer.gui.tools.dialogs.ConfirmDialog; + + +/** + * Displays a confirm dialog on close if unsaved changed exist + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public class CancelEditingAction extends ResourceAction { + + private static final long serialVersionUID = 1L; + + private final Window window; + private final transient BooleanSupplier hasUnsavedChanges; + + /** + * Creates a new CancelEditingAction + * + * @param dialog + * the window that should be closed + * @param i18nKey + * the i18n key used for both the resource and the ConfirmDialog + * @param hasUnsavedChanges + * supplier if unsaved changes exists + */ + public CancelEditingAction(Window dialog, String i18nKey, BooleanSupplier hasUnsavedChanges) { + super(i18nKey); + this.window = dialog; + this.hasUnsavedChanges = hasUnsavedChanges; + } + + @Override + protected void loggedActionPerformed(ActionEvent e) { + if (!hasUnsavedChanges.getAsBoolean() || + ConfirmDialog.YES_OPTION == ConfirmDialog.showConfirmDialogWithOptionalCheckbox(window, + getKey(), ConfirmDialog.YES_NO_OPTION, null, + ConfirmDialog.NO_OPTION, false)) { + window.dispose(); + } + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/actions/InjectParametersAction.java b/src/main/java/com/rapidminer/connection/gui/actions/InjectParametersAction.java new file mode 100644 index 000000000..331fea759 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/actions/InjectParametersAction.java @@ -0,0 +1,82 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.actions; + +import java.awt.Dimension; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import com.rapidminer.connection.gui.InjectParametersDialog; +import com.rapidminer.connection.gui.model.ConnectionParameterModel; +import com.rapidminer.connection.gui.model.InjectParametersModel; +import com.rapidminer.connection.valueprovider.ValueProvider; +import com.rapidminer.gui.tools.ResourceAction; + + +/** + * Inject Parameters Action + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public class InjectParametersAction extends ResourceAction { + + private final Window parent; + private final String type; + private final transient Consumer> save; + private final Supplier> injectableParameters; + private final List valueProviders; + + + /** + * Creates a new InjectParametersAction + * + * @param parent + * the parent window + * @param type + * the connection type + * @param injectableParameters + * supplier of the injectable parameters. Asked when the dialog is created + * @param valueProviders + * names of the available value providers + */ + public InjectParametersAction(Window parent, String type, Supplier> injectableParameters, List valueProviders, Consumer> saveCallback) { + super(true, "inject_connection_parameter"); + this.parent = parent; + this.type = type; + this.save = saveCallback; + this.injectableParameters = injectableParameters; + this.valueProviders = valueProviders; + } + + + @Override + protected void loggedActionPerformed(ActionEvent e) { + InjectParametersModel data = new InjectParametersModel(injectableParameters.get(), valueProviders); + InjectParametersDialog dialog = new InjectParametersDialog(parent, type, getKey(), data); + dialog.setSize(new Dimension(650, 400)); + dialog.setVisible(true); + if (dialog.wasConfirmed()) { + save.accept(data.getParameters()); + } + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/actions/OpenEditConnectionAction.java b/src/main/java/com/rapidminer/connection/gui/actions/OpenEditConnectionAction.java new file mode 100644 index 000000000..fdb7ad111 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/actions/OpenEditConnectionAction.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.actions; + +import java.awt.Window; +import java.awt.event.ActionEvent; + +import com.rapidminer.connection.gui.ConnectionEditDialog; +import com.rapidminer.connection.gui.dto.ConnectionInformationHolder; +import com.rapidminer.gui.tools.ResourceAction; + + +/** + * Closes the view window and opens the Dialog with edit rights + * + * @author Jonas Wilms-Pfau + * @since 9.3.0 + */ +public class OpenEditConnectionAction extends ResourceAction { + + private final Window parent; + private final transient ConnectionInformationHolder connection; + + public OpenEditConnectionAction(Window parent, ConnectionInformationHolder holder) { + super("edit_connection"); + this.parent = parent; + this.connection = holder; + } + + @Override + protected void loggedActionPerformed(ActionEvent e) { + String currentTabTitle = null; + if (parent instanceof ConnectionEditDialog) { + currentTabTitle = ((ConnectionEditDialog) parent).getCurrentTabTitle(); + } + this.parent.dispose(); + + ConnectionEditDialog dialog = new ConnectionEditDialog(connection); + if (currentTabTitle != null) { + dialog.showTab(currentTabTitle); + } + dialog.setVisible(true); + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/actions/SaveConnectionAction.java b/src/main/java/com/rapidminer/connection/gui/actions/SaveConnectionAction.java new file mode 100644 index 000000000..275e661f4 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/actions/SaveConnectionAction.java @@ -0,0 +1,193 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.actions; + +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.swing.JButton; + +import com.rapidminer.connection.ConnectionHandlerRegistry; +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.ConnectionInformationContainerIOObject; +import com.rapidminer.connection.gui.ConnectionGUI; +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.connection.util.TestResult; +import com.rapidminer.connection.util.ValidationResult; +import com.rapidminer.connection.valueprovider.ValueProvider; +import com.rapidminer.connection.valueprovider.handler.ValueProviderHandler; +import com.rapidminer.connection.valueprovider.handler.ValueProviderHandlerRegistry; +import com.rapidminer.gui.tools.ProgressThread; +import com.rapidminer.gui.tools.ResourceAction; +import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.gui.tools.dialogs.ConfirmDialog; +import com.rapidminer.repository.RepositoryException; +import com.rapidminer.repository.RepositoryLocation; +import com.rapidminer.repository.RepositoryManager; +import com.rapidminer.tools.usagestats.ActionStatisticsCollector; + + +/** + * Action for saving a connection + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public class SaveConnectionAction extends ResourceAction { + + /** + * The progress thread id consists of {@value #PROGRESS_THREAD_ID_PREFIX} followed by the connection location. + */ + public static final String PROGRESS_THREAD_ID_PREFIX = "saving_connection"; + + private final Window parent; + private final transient ConnectionGUI gui; + private final transient Supplier locationSupplier; + private final Consumer testResultConsumer; + + + public SaveConnectionAction(Window parent, ConnectionGUI gui, Supplier location, Consumer testResultConsumer, String i18nKey) { + super(i18nKey); + this.parent = parent; + this.gui = gui; + this.locationSupplier = location; + this.testResultConsumer = testResultConsumer; + } + + @Override + protected void loggedActionPerformed(ActionEvent e) { + if(!gui.preSaveCheck()){ + return; + } + final ConnectionInformation connection = gui.getConnection(); + if (!validOrSaveAnyway(connection)) { + return; + } + parent.setVisible(false); + final RepositoryLocation location = locationSupplier.get(); + ProgressThread progressThread = new ProgressThread(PROGRESS_THREAD_ID_PREFIX, false, location.toString()) { + @Override + public void run() { + ConnectionInformationContainerIOObject ioobject = new ConnectionInformationContainerIOObject(connection); + try { + RepositoryManager.getInstance(null).store(ioobject, location, null); + SwingTools.invokeLater(parent::dispose); + logConnectionStatus(ActionStatisticsCollector.ARG_SUCCESS, connection); + } catch (RepositoryException e) { + SwingTools.invokeLater(() -> parent.setVisible(true)); + SwingTools.showSimpleErrorMessage(parent, "saving_connection_failed", e, connection.getConfiguration().getName(), location.toString(), e.getMessage()); + logConnectionStatus(ActionStatisticsCollector.ARG_FAILED, connection); + } + } + + @Override + public String getID() { + return super.getID() + location; + } + }; + progressThread.setIndeterminate(true); + progressThread.start(); + } + + /** + * Logs the connection and the status of the saving (success/failed). + */ + private void logConnectionStatus(String status, ConnectionInformation connection) { + ActionStatisticsCollector.INSTANCE.log(ActionStatisticsCollector.TYPE_CONNECTION, + connection.getConfiguration().getType(), status + ActionStatisticsCollector.ARG_SPACER+ + ActionStatisticsCollector.getConnectionInjections(connection.getConfiguration())); + } + + /** + * Verifies the given connection, displays a save anyway dialog if necessary + * + * @param connection + * the connection to verify + * @return {@code true} if the connection is valid, or the user decided to save anyway + */ + private boolean validOrSaveAnyway(ConnectionInformation connection) { + ValidationResult valueProviderValidation = checkValueProviders(connection); + String type = connection.getConfiguration().getType(); + ValidationResult connectionValidation = ValidationResult.nullable(); + if (ConnectionHandlerRegistry.getInstance().isTypeKnown(type)) { + connectionValidation = ConnectionHandlerRegistry.getInstance().getHandler(type).validate(connection); + } + ValidationResult result = ValidationResult.merge(valueProviderValidation, connectionValidation); + + // notify UI about validation results + testResultConsumer.accept(result); + + if (!result.getType().equals(TestResult.ResultType.FAILURE)) { + return true; + } + + String message = ConnectionI18N.getConnectionGUIMessageOrNull(result.getMessageKey(), result.getArguments()); + if (message == null) { + message = ConnectionI18N.getConnectionGUIMessage("validation.failed"); + } + final String i18nMessage = message; + + return SwingTools.invokeAndWaitWithResult(() -> { + ConfirmDialog dialog = new ConfirmDialog(parent, "save_invalid_connection", ConfirmDialog.OK_CANCEL_OPTION, false, i18nMessage) { + @Override + protected JButton makeOkButton() { + return makeOkButton("connection.save_anyway"); + } + + @Override + protected JButton makeCancelButton() { + return makeCancelButton("connection.back_to_editing"); + } + }; + dialog.setVisible(true); + return dialog.wasConfirmed(); + }); + } + + /** + * Check if the {@link ValueProvider ValueProviders} used by the given {@link ConnectionInformation} contain setup + * errors and collects those. + * + * @param connection + * the connection that needs to be checked for value provider misconfiguration + * @return the pessimistic merge result for all the checked value providers. + * @see ValidationResult#merge(ValidationResult...) + */ + static ValidationResult checkValueProviders(ConnectionInformation connection) { + Objects.requireNonNull(connection); + + final List results = new ArrayList<>(); + for (ValueProvider valueProvider : connection.getConfiguration().getValueProviders()) { + if (ValueProviderHandlerRegistry.getInstance().isTypeKnown(valueProvider.getType())) { + ValueProviderHandler handler = ValueProviderHandlerRegistry.getInstance().getHandler(valueProvider.getType()); + ValidationResult validate = handler.validate(valueProvider, connection); + results.add(validate); + } else { + results.add(ValidationResult.failure("save_unknown_vp", null, valueProvider.getType())); + } + } + + return ValidationResult.merge(results.toArray(new ValidationResult[0])); + } + +} diff --git a/src/main/java/com/rapidminer/connection/gui/actions/TestConnectionAction.java b/src/main/java/com/rapidminer/connection/gui/actions/TestConnectionAction.java new file mode 100644 index 000000000..252b62fd8 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/actions/TestConnectionAction.java @@ -0,0 +1,193 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.actions; + +import java.awt.event.ActionEvent; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.logging.Level; +import javax.swing.Icon; +import javax.swing.ImageIcon; + +import com.rapidminer.connection.ConnectionHandler; +import com.rapidminer.connection.ConnectionHandlerRegistry; +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.connection.util.TestExecutionContextImpl; +import com.rapidminer.connection.util.TestResult; +import com.rapidminer.connection.util.ValidationResult; +import com.rapidminer.gui.tools.ProgressThread; +import com.rapidminer.gui.tools.ProgressThreadStoppedException; +import com.rapidminer.gui.tools.ResourceAction; +import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.tools.LogService; +import com.rapidminer.tools.usagestats.ActionStatisticsCollector; + + +/** + * The action that triggers when a {@link ConnectionInformation} is tested in the connection UI. + * + * @author Jonas Wilms-Pfau + * @since 9.3.0 + */ +public class TestConnectionAction extends ResourceAction { + + private static final ImageIcon CANCEL_ICON = SwingTools.createIcon("24/" + ConnectionI18N.getConnectionGUIMessage("test.cancel.icon")); + private static final String CANCEL_TESTING = ConnectionI18N.getConnectionGUILabel("test.cancel_testing"); + + private static final TestResult NOT_SUPPORTED = new TestResult(TestResult.ResultType.NOT_SUPPORTED, "test.not_registered", null); + private static final TestResult TEST_RUNNING = new TestResult(TestResult.ResultType.NONE, "test.running", null); + + private final transient Supplier connectionSupplier; + private final transient Consumer setTestResult; + /** Boolean indicating if it's testing or not */ + private final AtomicBoolean isTesting = new AtomicBoolean(false); + /** Current call count */ + private final AtomicInteger callCount = new AtomicInteger(0); + + private final String name; + private final transient Icon icon; + + private transient ProgressThread testThread; + + + /** + * Creates a new test connection action + * + * @param connection supplier for a connection to test + * @param setResult consumer that accepts the test results + */ + public TestConnectionAction(Supplier connection, Consumer setResult) { + super(false, "test_connection"); + this.connectionSupplier = connection; + this.setTestResult = setResult; + this.name = (String) getValue(NAME); + this.icon = (Icon) getValue(LARGE_ICON_KEY); + } + + @Override + protected void loggedActionPerformed(ActionEvent e) { + final int currentCallCount = callCount.incrementAndGet(); + final ProgressThread currentTestThread = testThread; + if (isTesting.compareAndSet(true, false)) { + resetIcons(); + if (currentTestThread != null) { + currentTestThread.cancel(); + } + testThread = null; + setTestResult.accept(null); + return; + } else if (!isTesting.compareAndSet(false, true)) { + return; + } + + SwingTools.invokeAndWait(() -> { + setTestResult.accept(TEST_RUNNING); + putValue(NAME, CANCEL_TESTING); + putValue(LARGE_ICON_KEY, CANCEL_ICON); + firePropertyChange("icons", null, null); + }); + + testThread = new ProgressThread("test_connection") { + @Override + public void run() { + TestResult testResult = null; + ConnectionInformation connection = null; + try { + connection = connectionSupplier.get(); + checkCancelled(); + testResult = getTestResult(connection, this); + logTestResult(testResult, connection); + checkCancelled(); + } catch (ProgressThreadStoppedException ptse) { + testResult = null; + throw ptse; + } catch (Throwable t) { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.connection.gui.actions.TestConnectionAction.testing_failed", t); + testResult = TestResult.failure("test.unexpected_error", t.getMessage()); + logTestResult(testResult, connection); + } finally { + if (currentCallCount == callCount.get() && isTesting.compareAndSet(true, false)) { + resetIcons(); + Optional.ofNullable(testResult).ifPresent(setTestResult); + } + } + } + }; + testThread.start(); +} + + /** + * Logs the testResult for the connection. + */ + private void logTestResult(TestResult testResult, ConnectionInformation connection) { + String type = connection != null ? connection.getConfiguration().getType() : "unknown"; + String logResult = testResult.getType() + ActionStatisticsCollector.ARG_SPACER + testResult.getMessageKey(); + ActionStatisticsCollector.INSTANCE.log(ActionStatisticsCollector.TYPE_CONNECTION_TEST, type, logResult); + } + + /** + * Tests the given connection + * @param connection the connection to test + * @return the test result + */ + private TestResult getTestResult(ConnectionInformation connection, ProgressThread thread) { + if (connection == null) { + return TestResult.nullable(); + } + + String type = connection.getConfiguration().getType(); + if (!ConnectionHandlerRegistry.getInstance().isTypeKnown(type)) { + return NOT_SUPPORTED; + } + ConnectionHandler handler = ConnectionHandlerRegistry.getInstance().getHandler(type); + + // check validation to make sure we catch basic errors before actually testing + ValidationResult valResult = handler.validate(connection); + if (valResult.getType() == ValidationResult.ResultType.FAILURE) { + return valResult; + } + + ValidationResult validationResult = SaveConnectionAction.checkValueProviders(connection); + if (validationResult.getType() == ValidationResult.ResultType.FAILURE) { + return validationResult; + } + + // only if validate succeeds, go to actual test + TestResult result = handler.test(new TestExecutionContextImpl<>(connection, thread)); + if (result == null) { + return NOT_SUPPORTED; + } + return result; + } + + /** + * Resets the icons to the regular icons + */ + private void resetIcons() { + SwingTools.invokeAndWait(() -> { + putValue(NAME, name); + putValue(LARGE_ICON_KEY, icon); + firePropertyChange("icons", null, null); + }); + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/components/ConnectionInfoPanel.java b/src/main/java/com/rapidminer/connection/gui/components/ConnectionInfoPanel.java new file mode 100644 index 000000000..0347c00b1 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/components/ConnectionInfoPanel.java @@ -0,0 +1,334 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.components; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.Insets; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import javax.swing.BorderFactory; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSeparator; +import javax.swing.JTextArea; + +import com.rapidminer.connection.ConnectionHandlerRegistry; +import com.rapidminer.connection.gui.listener.TextChangedDocumentListener; +import com.rapidminer.connection.gui.model.ConnectionModel; +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.gui.look.Colors; +import com.rapidminer.gui.tools.ExtendedJScrollPane; +import com.rapidminer.gui.tools.IconSize; +import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.gui.tools.components.FixedWidthLabel; +import com.rapidminer.gui.tools.components.LinkLocalButton; +import com.rapidminer.repository.RepositoryLocation; +import com.rapidminer.tools.FontTools; +import com.rapidminer.tools.I18N; +import com.rapidminer.tools.OperatorService; +import com.rapidminer.tools.plugin.Plugin; + +import javafx.beans.property.StringProperty; +import javafx.collections.ObservableList; + + +/** + * Content of the Info tab in the Edit Connection Dialog + * + * @author Jonas Wilms-Pfau + * @since 9.3.0 + */ +public class ConnectionInfoPanel extends JPanel { + + private static final Dimension TEXTAREA_SIZE = new Dimension(1, 90); + private static final Font OPEN_SANS_12 = FontTools.getFont("Open Sans", Font.PLAIN, 12); + private static final Font OPEN_SANS_20 = FontTools.getFont("Open Sans", Font.PLAIN, 20); + private static final Font OPEN_SANS_SEMIBOLD_14 = FontTools.getFont("Open Sans Semibold", Font.BOLD, 14); + private static final Font OPEN_SANS_SEMIBOLD_24 = FontTools.getFont("Open Sans Semibold", Font.BOLD, 24); + public static final Color UNKNOWN_TYPE_COLOR = new Color(111, 111, 112); + + private final boolean editable; + private final boolean isTypeKnown; + + public ConnectionInfoPanel(ConnectionModel connection) { + this(connection, true); + } + + /** + * Creates a connection information display panel. + * + * @param connection + * the model of the connection to show + * @param showDescriptions + * if {@code true} descriptions for the headers are displayed + */ + public ConnectionInfoPanel(ConnectionModel connection, boolean showDescriptions) { + super(new GridBagLayout()); + this.editable = connection.isEditable(); + String connectionType = connection.getType(); + isTypeKnown = ConnectionHandlerRegistry.getInstance().isTypeKnown(connectionType); + + // header with icon, name, and type + GridBagConstraints gbc = new GridBagConstraints(); + JPanel headerPanel = new JPanel(new GridBagLayout()); + GridBagConstraints headerGbc = new GridBagConstraints(); + headerGbc.gridx = 0; + headerGbc.gridy = 0; + headerGbc.anchor = GridBagConstraints.WEST; + headerGbc.gridheight = 2; + headerGbc.insets = new Insets(0, 0, 0, 10); + headerPanel.add(new JLabel(ConnectionI18N.getConnectionIcon(connectionType, IconSize.HUGE)), headerGbc); + + headerGbc.gridx = 1; + headerGbc.gridheight = 1; + headerGbc.insets = new Insets(0, 0, 0, 0); + JLabel nameLabel = new JLabel(connection.getName()); + nameLabel.setToolTipText(connection.getName()); + nameLabel.setFont(OPEN_SANS_SEMIBOLD_24); + headerPanel.add(nameLabel, headerGbc); + + headerGbc.gridx = 1; + headerGbc.gridy += 1; + headerGbc.gridheight = 1; + JComponent typeComponent = createTypeComponent(connectionType); + headerPanel.add(typeComponent, headerGbc); + + gbc.gridx = 0; + gbc.gridy = 0; + gbc.weightx = 1.0; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.anchor = GridBagConstraints.NORTHWEST; + gbc.insets = new Insets(25, 25, 15, 25); + JPanel headerOuterPanel = new JPanel(new BorderLayout()); + headerOuterPanel.add(headerPanel, BorderLayout.WEST); + add(headerOuterPanel, gbc); + + gbc.gridy += 1; + gbc.weightx = 0.8; + gbc.insets = new Insets(0, 20, 0, 200); + JSeparator separator = new JSeparator(); + add(separator, gbc); + + // body with location, description, and tags + JPanel bodyPanel = new JPanel(new GridLayout(3, 2, 30, 20)); + bodyPanel.add(createDescriptionPanel(ConnectionI18N.getConnectionGUILabel("location"), + ConnectionI18N.getConnectionGUILabel("location_description"), showDescriptions)); + + String repositoryName = connection.getLocation() != null ? + RepositoryLocation.REPOSITORY_PREFIX + connection.getLocation().getRepositoryName() : ""; + JTextArea locArea = new JTextArea(repositoryName); + locArea.setEditable(false); + locArea.setHighlighter(null); + locArea.setLineWrap(true); + locArea.setComponentPopupMenu(null); + locArea.setInheritsPopupMenu(false); + locArea.setBackground(getBackground()); + locArea.setBorder(BorderFactory.createEmptyBorder()); + locArea.setFont(OPEN_SANS_12); + if (!isTypeKnown) { + locArea.setForeground(UNKNOWN_TYPE_COLOR); + } + + bodyPanel.add(locArea); + + bodyPanel.add(createDescriptionPanel(ConnectionI18N.getConnectionGUILabel("description"), + ConnectionI18N.getConnectionGUILabel("description_description"), showDescriptions)); + + bodyPanel.add(createTextArea(connection.descriptionProperty())); + + bodyPanel.add(createDescriptionPanel(ConnectionI18N.getConnectionGUILabel("tags"), + ConnectionI18N.getConnectionGUILabel("tags_description"), showDescriptions)); + + bodyPanel.add(createTagPanel(connection.getTags(), connection::setTags)); + + gbc.gridy += 1; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + gbc.fill = GridBagConstraints.BOTH; + gbc.insets = new Insets(15, 25, 10, 25); + add(bodyPanel, gbc); + } + + /** + * Create a description panel. + */ + private JPanel createDescriptionPanel(String header, String description, boolean showDescription) { + JPanel descPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + + gbc.gridx = 0; + gbc.gridy = 0; + gbc.anchor = GridBagConstraints.WEST; + gbc.weightx = 1.0; + gbc.fill = GridBagConstraints.HORIZONTAL; + JLabel headerLabel = new JLabel(header); + headerLabel.setFont(OPEN_SANS_SEMIBOLD_14); + descPanel.add(headerLabel, gbc); + + if (showDescription) { + gbc.gridy += 1; + JLabel descLabel = new FixedWidthLabel(300, description); + descLabel.setFont(OPEN_SANS_12); + descPanel.add(descLabel, gbc); + if (!isTypeKnown) { + descLabel.setForeground(UNKNOWN_TYPE_COLOR); + } + } + + gbc.gridy += 1; + gbc.weighty = 1.0; + gbc.fill = GridBagConstraints.VERTICAL; + descPanel.add(new JLabel(), gbc); + + if (!isTypeKnown) { + headerLabel.setForeground(UNKNOWN_TYPE_COLOR); + } + + return descPanel; + } + + /** + * Creates the type component which is either the type i18n and the origin name (if known), or an indicator that the + * origin is unknown and a link to check the marketplace. + * + * @return the component + */ + private JComponent createTypeComponent(String connectionType) { + JComponent typeComponent; + int prefixSeparatorIndex = connectionType.indexOf(':'); + String namespace = null; + String providerName; + if (prefixSeparatorIndex > 0) { + namespace = connectionType.substring(0, prefixSeparatorIndex); + providerName = Optional.ofNullable(Plugin.getPluginByExtensionId("rmx_" + namespace)).map(Plugin::getName).orElse(null); + } else { + providerName = OperatorService.RAPID_MINER_CORE_PREFIX; + } + + if (isTypeKnown) { + String type = "" + ConnectionI18N.getTypeName(connectionType); + if (providerName != null) { + type += " (" + providerName + ")"; + } + type += ""; + typeComponent = new JLabel(type); + typeComponent.setFont(OPEN_SANS_20); + } else { + if (namespace != null) { + JPanel unknownPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + + gbc.gridx = 0; + gbc.gridy = 0; + gbc.anchor = GridBagConstraints.WEST; + + JLabel unknownLabel = new JLabel(I18N.getGUILabel("connection.unknown_type.label")); + unknownLabel.setFont(OPEN_SANS_20); + unknownLabel.setForeground(UNKNOWN_TYPE_COLOR); + unknownLabel.setToolTipText(connectionType.substring(prefixSeparatorIndex + 1)); + unknownPanel.add(unknownLabel, gbc); + gbc.insets.right = 20; + gbc.gridx += 1; + unknownPanel.add(new LinkLocalButton(SwingTools.createMarketplaceDownloadActionForNamespace("connection.install_extension_unknown_type", namespace)), gbc); + typeComponent = unknownPanel; + } else { + // old Studio with new connection? No prefix in type, so we cannot help with anything + typeComponent = new JLabel(I18N.getGUILabel("connection.unknown_type_no_help.label", connectionType)); + typeComponent.setFont(OPEN_SANS_20); + typeComponent.setForeground(UNKNOWN_TYPE_COLOR); + } + } + + return typeComponent; + } + + /** + * Adds a JTextArea with the given text + * + * @param text + * The text + */ + private JComponent createTextArea(StringProperty text) { + // Without the TextArea the GUI is collapsing + JTextArea multi = new JTextArea(text.get()); + multi.setFont(OPEN_SANS_12); + // update text + text.addListener(l -> { + if (!multi.getText().equals(text.get())) { + SwingTools.invokeLater(() -> multi.setText(text.get())); + } + }); + multi.setWrapStyleWord(true); + multi.setLineWrap(true); + multi.setEditable(editable); + if (!editable) { + multi.setHighlighter(null); + multi.setComponentPopupMenu(null); + multi.setInheritsPopupMenu(false); + multi.setBackground(getBackground()); + multi.setBorder(BorderFactory.createEmptyBorder()); + } else { + multi.getDocument().addDocumentListener(new TextChangedDocumentListener(text)); + multi.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); + } + if (!isTypeKnown) { + multi.setForeground(UNKNOWN_TYPE_COLOR); + } + return createWithScrollPane(multi); + } + + /** + * Adds the TagPanel + * + * @param tags + * The tags + * @param setTags + * A method to set the Tags + */ + private JComponent createTagPanel(ObservableList tags, Consumer> setTags) { + return createWithScrollPane(new ConnectionTagEditPanel(tags, setTags, editable, isTypeKnown)); + } + + + /** + * Creates the component inside a JScrollPane. + * + * @param component + * the component to scroll + */ + private JScrollPane createWithScrollPane(JComponent component) { + JScrollPane scrollPane = new ExtendedJScrollPane(component); + scrollPane.setBorder(editable ? BorderFactory.createLineBorder(Colors.TEXTFIELD_BORDER) : BorderFactory.createEmptyBorder()); + scrollPane.setBackground(component.getBackground()); + scrollPane.setMinimumSize(TEXTAREA_SIZE); + scrollPane.setPreferredSize(TEXTAREA_SIZE); + scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + return scrollPane; + } + +} diff --git a/src/main/java/com/rapidminer/connection/gui/components/ConnectionParameterCheckBox.java b/src/main/java/com/rapidminer/connection/gui/components/ConnectionParameterCheckBox.java new file mode 100644 index 000000000..881e8de34 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/components/ConnectionParameterCheckBox.java @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.components; + +import java.util.Objects; +import javax.swing.JCheckBox; + +import com.rapidminer.connection.gui.model.ConnectionParameterModel; +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.gui.StaticButtonModel; + +/** + * A {@link JCheckBox} representing a boolean {@link ConnectionParameterModel}. Looks active but is inert in view mode. + * + * @author Jan Czogalla + * @since 9.3 + */ +public class ConnectionParameterCheckBox extends JCheckBox { + + public ConnectionParameterCheckBox(String type, ConnectionParameterModel parameter) { + this(type, parameter.getGroupName(), parameter.getName()); + boolean editable = parameter.isEditable(); + setEnabled((parameter.isEnabled() && editable)); + setSelected(Boolean.parseBoolean(parameter.getValue())); + if (!editable) { + setModel(new StaticButtonModel(isSelected())); + setFocusPainted(false); + return; + } + parameter.enabledProperty().addListener((observable, oldValue, newValue) -> setEnabled(newValue)); + parameter.valueProperty().addListener((observable, oldValue, newValue) -> { + if (Objects.equals(oldValue, newValue)) { + return; + } + setSelected(Boolean.parseBoolean(newValue)); + }); + addChangeListener(e -> parameter.setValue(Boolean.toString(isSelected()))); + } + + public ConnectionParameterCheckBox(String type, String groupKey, String parameterKey) { + super(ConnectionI18N.getParameterName(type, groupKey, parameterKey, parameterKey)); + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/components/ConnectionParameterLabel.java b/src/main/java/com/rapidminer/connection/gui/components/ConnectionParameterLabel.java new file mode 100644 index 000000000..68c625ba2 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/components/ConnectionParameterLabel.java @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.components; + +import java.awt.Font; +import javax.swing.ImageIcon; +import javax.swing.JLabel; + +import com.rapidminer.connection.gui.model.ConnectionParameterModel; +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.gui.tools.SwingTools; + + +/** + * Connection Parameter Label + * + * @since 9.3 + * @author Jonas Wilms-Pfau + */ +public class ConnectionParameterLabel extends JLabel { + + private static final ImageIcon WARNING_ICON = SwingTools.createIcon("16/" + ConnectionI18N.getConnectionGUIMessage("validation.warning.icon")); + + /** + * Creates a new {@link ConnectionParameterLabel} + *

+ * Displays a warning icon if an {@link ConnectionParameterModel#getValidationError() validation error} exists + *

+ * + * @param type + * the {@link com.rapidminer.connection.gui.model.ConnectionModel#getType() connection type} + * @param parameter + * the parameter for which this label is displayed + */ + public ConnectionParameterLabel(String type, ConnectionParameterModel parameter) { + this(type, parameter.getGroupName(), parameter.getName()); + parameter.validationErrorProperty().addListener((observable, oldValue, newValue) -> { + if (oldValue == null && newValue != null) { + setFont(getFont().deriveFont(Font.BOLD)); + setIcon(WARNING_ICON); + } else if (oldValue != null && newValue == null) { + setFont(getFont().deriveFont(Font.PLAIN)); + setIcon(null); + } + }); + parameter.enabledProperty().addListener((observable, oldValue, newValue) -> setEnabled(newValue)); + setEnabled((parameter.isEnabled())); + } + + /** + * Creates an {@link ConnectionParameterLabel} for given parameter + * + * @param type the {@link com.rapidminer.connection.gui.model.ConnectionModel#getType() connection type} + * @param groupKey {@link com.rapidminer.connection.gui.model.ConnectionParameterGroupModel#getName() parameter group name} + * @param parameterKey {@link ConnectionParameterModel#getName() parameter name} + */ + public ConnectionParameterLabel(String type, String groupKey, String parameterKey) { + super(ConnectionI18N.getParameterName(type, groupKey, parameterKey, parameterKey)); + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/components/ConnectionParameterTextField.java b/src/main/java/com/rapidminer/connection/gui/components/ConnectionParameterTextField.java new file mode 100644 index 000000000..c35f9964a --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/components/ConnectionParameterTextField.java @@ -0,0 +1,132 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.components; + +import java.awt.Color; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import javax.swing.BorderFactory; +import javax.swing.JPanel; +import javax.swing.JPasswordField; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; +import javax.swing.border.Border; +import javax.swing.text.JTextComponent; + +import org.apache.commons.lang.StringUtils; + +import com.rapidminer.connection.gui.listener.TextChangedDocumentListener; +import com.rapidminer.connection.gui.model.ConnectionParameterModel; +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.gui.tools.SwingTools; + + +/** + * Connection Parameter TextField + *

+ * Displays an simple input field. With support for: + *

    + *
  • {@link ConnectionParameterModel#isInjected() Injection: Displays an injection icon}
  • + *
  • {@link ConnectionParameterModel#isEnabled() Enabled/Disabled: Disables the element if needed}
  • + *
  • {@link ConnectionParameterModel#isEncrypted() Encryption: Displays an password field if needed}
  • + *
  • {@link ConnectionParameterModel#setValue(String) Updates the underlying parameter model}
  • + *
  • {@link ConnectionParameterModel#valueProperty() Listens to parameter values from outside}
  • + *
+ *

+ * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public class ConnectionParameterTextField extends JPanel { + + public static final Border DISABLED_BORDER = BorderFactory.createEmptyBorder(2, 2, 2, 2); + private JTextComponent editComponent; + private final String type; + private transient ConnectionParameterModel parameter; + + + /** + * Creates a new {@link ConnectionParameterTextField text field} for the {@link ConnectionParameterModel parameter} + * + * @param type + * the {@link com.rapidminer.connection.gui.model.ConnectionModel#getType() connection type} + * @param parameter + * the parameter + */ + public ConnectionParameterTextField(String type, ConnectionParameterModel parameter) { + super(new GridBagLayout()); + this.type = type; + this.parameter = parameter; + editComponent = createEditComponent(); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.weightx = 1.0; + gbc.fill = GridBagConstraints.HORIZONTAL; + add(new InjectableComponentWrapper(editComponent, parameter), gbc); + parameter.enabledProperty().addListener((observable, oldValue, newValue) -> toggleDisabledState()); + toggleDisabledState(); + if (!parameter.isEditable()) { + editComponent.setBorder(DISABLED_BORDER); + editComponent.setEditable(false); + editComponent.setBackground(new Color(255, 255, 255, 0)); + editComponent.setOpaque(false); + // Set to ********, in case the password is set and in view mode + if (parameter.isEncrypted() && !StringUtils.isEmpty(parameter.getValue())) { + editComponent.setText(ConnectionI18N.getConnectionGUILabel("placeholder_encrypted")); + } + toggleDisabledState(); + return; + } + parameter.valueProperty().addListener((observable, oldValue, newValue) -> { + if (!editComponent.getText().equals(newValue)) { + SwingUtilities.invokeLater(() -> editComponent.setText(newValue)); + } + }); + } + + private void toggleDisabledState() { + editComponent.setEnabled((parameter.isEnabled())); + } + + /** + * @return the component that is used for encrypted parameters + */ + protected JTextComponent getEncryptedTextComponent() { + return new JPasswordField(15); + } + + /** + * @return the editable text component + */ + protected JTextComponent getTextComponent() { + return new JTextField(15); + } + + private JTextComponent createEditComponent() { + JTextComponent textComponent = parameter.isEncrypted() ? getEncryptedTextComponent() : getTextComponent(); + textComponent.setText(parameter.getValue()); + if (parameter.isEditable()) { + textComponent.getDocument().addDocumentListener(new TextChangedDocumentListener(parameter.valueProperty())); + SwingTools.setPrompt(ConnectionI18N.getParameterPrompt(type, parameter.getGroupName(), parameter.getName(), null), textComponent); + } + return textComponent; + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/components/ConnectionSourcesPanel.java b/src/main/java/com/rapidminer/connection/gui/components/ConnectionSourcesPanel.java new file mode 100644 index 000000000..2dc2e2c7f --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/components/ConnectionSourcesPanel.java @@ -0,0 +1,222 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.components; + +import java.awt.BorderLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.util.logging.Level; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSeparator; +import javax.swing.SwingConstants; +import javax.swing.border.EmptyBorder; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.gui.AbstractConnectionGUI; +import com.rapidminer.connection.gui.ValueProviderGUIProvider; +import com.rapidminer.connection.gui.ValueProviderGUIRegistry; +import com.rapidminer.connection.gui.model.ConnectionModel; +import com.rapidminer.connection.gui.model.ConnectionModelConverter; +import com.rapidminer.connection.gui.model.ValueProviderModel; +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.connection.util.TestResult; +import com.rapidminer.connection.valueprovider.ValueProvider; +import com.rapidminer.connection.valueprovider.handler.ValueProviderHandler; +import com.rapidminer.connection.valueprovider.handler.ValueProviderHandlerRegistry; +import com.rapidminer.gui.look.icons.IconFactory; +import com.rapidminer.gui.tools.ExtendedJScrollPane; +import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.gui.tools.components.LinkLocalButton; +import com.rapidminer.tools.I18N; +import com.rapidminer.tools.LogService; + +import javafx.collections.ObservableList; + + +/** + * Panel to show or edit the parameters of {@link ValueProvider ValueProviders} for a {@link ConnectionModel}. Register + * custom UI providers in {@link ValueProviderGUIRegistry}. For an example see DEFAULT_VP_GUI_PROVIDER in + * {@link ValueProviderGUIRegistry} + * + * @author Andreas Timm + * @since 9.3 + */ +public class ConnectionSourcesPanel extends JPanel { + /** + * Where to find the icon to be displayed for properly configured value providers + */ + public static final String NOT_WELL_CONFIGURED_VALUE_PROVIDER_ICON = "gui.dialog.connection.valueprovider.needs_configuration.icon"; + /** + * The icon to show for properly configured ValueProviders + */ + private static final ImageIcon ICON_WARNING = SwingTools.createIcon("16/" + I18N.getGUILabel("connection.unknown_vp.icon")); + /** + * The model containing copies of all the necessary information + */ + private final ConnectionModel connectionModel; + private final JDialog parent; + + /** + * Create a new instance for the given connectionModel. + * + * @param connectionModel + * The model containing all the {@link ValueProvider ValueProviders} to configure or show + */ + public ConnectionSourcesPanel(JDialog parent, ConnectionModel connectionModel) { + this.connectionModel = connectionModel; + this.parent = parent; + setLayout(new BorderLayout()); + JLabel label; + if (!connectionModel.isEditable()) { + if (connectionModel.getValueProviders().isEmpty()) { + label = new JLabel(I18N.getGUIMessage("gui.dialog.connection.valueprovider.header_information_viewmode_empty")); + } else { + label = new JLabel(I18N.getGUIMessage("gui.dialog.connection.valueprovider.header_information_viewmode")); + } + } else { + label = new JLabel(I18N.getGUIMessage("gui.dialog.connection.valueprovider.header_information")); + } + label.setBorder(new EmptyBorder(16, 32, 10, 16)); + add(label, BorderLayout.NORTH); + add(createConfigurationPanel(), BorderLayout.CENTER); + } + + /** + * Create a panel that contains all the {@link ValueProvider ValueProviders} and their configuration, depending on + * the connectionModel they are editable + * + * @return a scrollable panel + */ + private JComponent createConfigurationPanel() { + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.fill = GridBagConstraints.BOTH; + gbc.anchor = GridBagConstraints.NORTHWEST; + gbc.weightx = 1; + + JPanel allVPPanel = new JPanel(new GridBagLayout()); + + ConnectionInformation information = ConnectionModelConverter.getConnection(connectionModel); + final ObservableList valueProviders = connectionModel.valueProvidersProperty(); + for (ValueProvider valueProvider : valueProviders) { + if (allVPPanel.getComponentCount() > 0) { + final Insets insets = gbc.insets; + gbc.insets = new Insets(0, 16, 0, 0); + allVPPanel.add(new JSeparator(), gbc); + gbc.insets = insets; + } + allVPPanel.add(createVPPanel(valueProvider, information), gbc); + } + gbc.weighty = 1; + allVPPanel.add(new JPanel(), gbc); + JPanel toTheLeft = new JPanel(); + toTheLeft.setLayout(new BorderLayout()); + toTheLeft.add(allVPPanel, BorderLayout.CENTER); + final ExtendedJScrollPane scrollPane = new ExtendedJScrollPane(toTheLeft); + scrollPane.setBorder(null); + return scrollPane; + } + + /** + * Create a panel to see the configuration for the given {@link ValueProvider} + * + * @param provider + * the {@link ValueProvider} to be viewed or configured, depending on the global connectionModel#editable flag + * @param information + * the information to use for checking the vps + * @return a panel containing the configuration values + */ + private JPanel createVPPanel(ValueProvider provider, ConnectionInformation information) { + if (provider == null) { + return new JPanel(); + } + JPanel outerPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.WEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.gridwidth = 1; + gbc.gridy = 0; + gbc.insets = new Insets(8, 32, 8, 16); + gbc.weightx = 1; + + String valueProviderType = provider.getType(); + boolean typeKnown = ValueProviderHandlerRegistry.getInstance().isTypeKnown(valueProviderType); + final ValueProviderHandler handler = typeKnown ? ValueProviderHandlerRegistry.getInstance().getHandler(valueProviderType) : null; + int prefixSeparatorIndex = valueProviderType.indexOf(':'); + String namespace = null; + if (prefixSeparatorIndex > 0) { + namespace = valueProviderType.substring(0, prefixSeparatorIndex); + } + + Insets oldinsets = gbc.insets; + boolean showWarning = typeKnown && handler.validate(provider, information).getType() == TestResult.ResultType.FAILURE; + gbc.insets = new Insets(oldinsets.top, oldinsets.left - 20, 0, oldinsets.right); + + if (typeKnown) { + outerPanel.add(new JLabel("" + ConnectionI18N.getValueProviderTypeName(valueProviderType) + "", + showWarning ? ICON_WARNING : IconFactory.getEmptyIcon16x16(), SwingConstants.LEFT), gbc); + } else { + JLabel label = new JLabel("" + I18N.getGUILabel("connection.unknown_vp.label", valueProviderType) + "", + ICON_WARNING, SwingConstants.LEFT); + label.setIconTextGap(10); + outerPanel.add(label, gbc); + } + + gbc.gridy++; + gbc.insets = oldinsets; + gbc.insets.left += 10; + if (!typeKnown) { + JComponent valueComponent; + JPanel unknownPanel = new JPanel(new GridBagLayout()); + + GridBagConstraints innerGbc = new GridBagConstraints(); + innerGbc.gridx = 0; + innerGbc.gridy = 0; + if (namespace != null) { + unknownPanel.add(new LinkLocalButton(SwingTools.createMarketplaceDownloadActionForNamespace("connection.install_extension_unknown_vp", namespace)), innerGbc); + valueComponent = unknownPanel; + } else { + // old Studio with new value provider? No prefix in type, so we cannot help with anything + innerGbc.insets = new Insets(0, 8, 0, 0); + unknownPanel.add(new JLabel(I18N.getGUILabel("connection.unknown_vp_no_help.label", valueProviderType)), innerGbc); + valueComponent = unknownPanel; + } + outerPanel.add(valueComponent, gbc); + } else if (!handler.isConfigurable() && showWarning) { + JLabel warningLabel = new JLabel(ConnectionI18N.getConnectionGUIMessage(handler.validate(provider, + information).getMessageKey())); + JPanel wrapped = AbstractConnectionGUI.addInformationIcon(warningLabel, provider.getType(), + "valueprovider", "no_configuration", parent); + outerPanel.add(wrapped, gbc); + } else { + final ValueProviderGUIProvider guiProvider = ValueProviderGUIRegistry.INSTANCE.getGUIProvider(valueProviderType); + try { + outerPanel.add(guiProvider.createConfigurationComponent(parent, provider, ConnectionModelConverter.getConnection(connectionModel), connectionModel.isEditable()), gbc); + } catch (Exception e) { + LogService.getRoot().log(Level.SEVERE, "Creating the component to configure the value provider handler " + valueProviderType + " failed", e); + } + } + return outerPanel; + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/components/ConnectionTagEditPanel.java b/src/main/java/com/rapidminer/connection/gui/components/ConnectionTagEditPanel.java new file mode 100644 index 000000000..d7eca00f2 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/components/ConnectionTagEditPanel.java @@ -0,0 +1,220 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.components; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Cursor; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.geom.RoundRectangle2D; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.function.Consumer; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.border.Border; +import javax.swing.border.EmptyBorder; + +import org.jdesktop.swingx.WrapLayout; + +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.tools.FontTools; + +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + + +/** + * The panel to edit connection tags. + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public class ConnectionTagEditPanel extends JPanel { + + private static final String CLOSE_SYMBOL = "\uf2d7"; + private static final Font CLOSE_FONT = new Font("Ionicons", Font.PLAIN, 20); + private static final Color TAG_BACKGROUND = new Color(204, 203, 203); + private static final Color TAG_FOREGROUND = new Color(74, 74, 74); + private static final Color TAG_BACKGROUND_DISABLED = new Color(225, 225, 225); + private static final Color TAG_FOREGROUND_DISABLED = ConnectionInfoPanel.UNKNOWN_TYPE_COLOR; + private static final Font OPEN_SANS_12 = FontTools.getFont("Open Sans", Font.PLAIN, 12); + private static final Font OPEN_SANS_13 = FontTools.getFont("Open Sans", Font.PLAIN, 13); + + private final boolean editable; + private final transient Consumer> setTags; + private final LinkedHashMap tagToValue = new LinkedHashMap<>(); + private final boolean isEnabled; + + ConnectionTagEditPanel(ObservableList tags, Consumer> setTags, final boolean editable, final boolean enabled) { + super(new WrapLayout(FlowLayout.LEFT, editable ? 11 : 0, editable ? 11 : 0)); + this.editable = editable; + this.setTags = setTags; + this.isEnabled = enabled; + + if (tags.isEmpty() && !editable) { + JLabel noTagLabel = new JLabel(ConnectionI18N.getConnectionGUILabel("no_tags")); + noTagLabel.setFont(OPEN_SANS_12); + if (!enabled) { + noTagLabel.setForeground(ConnectionInfoPanel.UNKNOWN_TYPE_COLOR); + } + add(noTagLabel); + } + + for (String s : tags) { + addTag(s, -1); + } + + if (!editable) { + return; + } + // EDITABLE ONLY + + // Empty border to as margin for elements + setBorder(new EmptyBorder(0, 0, 2, 2)); + setBackground(Color.WHITE); + + // Allow insertion of new tags + JTextField addField = new JTextField(5); + addField.setFont(OPEN_SANS_13); + addField.setBorder(new EmptyBorder(1, 1, 1, 1)); + addField.addActionListener(e -> SwingTools.invokeLater(() -> createTag(setTags, addField))); + addField.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + createTag(setTags, addField); + } + }); + add(addField); + setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + // Show tooltip and focus input field on click + setToolTipText(ConnectionI18N.getConnectionGUILabel("add_tag_hint")); + addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + addField.requestFocus(); + } + }); + + // Check for external modifications + tags.addListener((ListChangeListener) (l -> { + if (!l.getList().equals(new ArrayList<>(tagToValue.values()))) { + SwingTools.invokeLater(() -> { + tagToValue.keySet().forEach(this::remove); + for (String s : l.getList()) { + addTag(s, -1); + } + revalidate(); + repaint(); + }); + } + })); + } + + /** + * Creates a new tag. + */ + private void createTag(Consumer> setTags, JTextField addField) { + String tag = addField.getText().trim(); + if (tag.isEmpty()) { + return; + } + addField.setText(""); + addTag(tag, tagToValue.size()); + setTags.accept(new ArrayList<>(tagToValue.values())); + revalidate(); + repaint(); + // Scroll to the end of the panel + scrollRectToVisible(new Rectangle(getX(), Integer.MAX_VALUE / 2, 1, 1)); + } + + /** + * Adds a tag UI element for the given String + * + * @param tag + * the tag to create + * @param pos + * the position at which to insert the tag, or -1 to append the component to the end + */ + private void addTag(String tag, int pos) { + JLabel tagLabel = new JLabel(tag); + tagLabel.setFont(OPEN_SANS_13); + tagLabel.setForeground(isEnabled ? TAG_FOREGROUND : TAG_FOREGROUND_DISABLED); + // Add border for more space + Border border = BorderFactory.createEmptyBorder(2, editable ? 3 : 10, 3, 11); + + JPanel roundBackground = new JPanel(new BorderLayout()) { + @Override + public void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.setPaint(isEnabled ? TAG_BACKGROUND : TAG_BACKGROUND_DISABLED); + // Prevent it from being cut off + g2.fill(new RoundRectangle2D.Double(0, 0, getWidth() - 1, getHeight() - 1, 6, 6)); + } + }; + + tagLabel.setBorder(border); + roundBackground.add(tagLabel, BorderLayout.CENTER); + JButton removeTag = new JButton(CLOSE_SYMBOL); + + if (editable) { + roundBackground.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + roundBackground.setBackground(getBackground()); + removeTag.setFont(CLOSE_FONT.deriveFont(1f * removeTag.getFont().getSize())); + removeTag.setContentAreaFilled(false); + removeTag.setBorderPainted(false); + removeTag.setBorder(BorderFactory.createLineBorder(getBackground())); + roundBackground.add(removeTag, BorderLayout.WEST); + tagToValue.put(roundBackground, tag); + removeTag.addActionListener(e -> SwingTools.invokeLater(() -> { + tagToValue.remove(roundBackground); + remove(roundBackground); + setTags.accept(new ArrayList<>(tagToValue.values())); + revalidate(); + repaint(); + })); + add(roundBackground, pos); + } else { + JPanel spacer = new JPanel(new BorderLayout()); + spacer.setBackground(getBackground()); + spacer.add(roundBackground, BorderLayout.CENTER); + // Don't use FlowLayout gaps in view mode since they are also added to the top and left side + spacer.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 5)); + add(spacer, pos); + } + + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/components/DeprecationWarning.java b/src/main/java/com/rapidminer/connection/gui/components/DeprecationWarning.java new file mode 100644 index 000000000..b92597ab8 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/components/DeprecationWarning.java @@ -0,0 +1,127 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.components; + +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import javax.swing.BorderFactory; +import javax.swing.Icon; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextArea; +import javax.swing.SwingConstants; + +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.gui.look.Colors; +import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.tools.I18N; + + +/** + * Deprecation warning panel + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public class DeprecationWarning extends JPanel { + + private static final String DEPRECATION_PREFIX = "deprecation.warning"; + private static final Icon WARNING_ICON = SwingTools.createIcon("24/" + "sign_warning.png"); + + /** + * The original content pane of the dialog + */ + private Container originalContentPane; + + /** + * Creates a deprecation panel for the given i18n key + * + * @param i18nKey + * the {i18nKey} part of gui.label.deprecation.warning.{i18nKey}.label + */ + public DeprecationWarning(String i18nKey) { + super(new BorderLayout()); + JPanel warning = new JPanel(new GridBagLayout()); + JLabel infoIcon = new JLabel(WARNING_ICON); + infoIcon.setVerticalAlignment(SwingConstants.CENTER); + GridBagConstraints gbc = new GridBagConstraints(); + warning.setBackground(Colors.WARNING_COLOR); + gbc.insets = new Insets(5, 5, 5, 5); + gbc.fill = GridBagConstraints.BOTH; + warning.add(infoIcon, gbc); + gbc.weightx = gbc.weighty = 1; + String fullKey = String.join(ConnectionI18N.KEY_DELIMITER, DEPRECATION_PREFIX, i18nKey, ConnectionI18N.LABEL_SUFFIX); + warning.add(createMultiLineLabel(I18N.getGUILabel(fullKey)), gbc); + add(warning, BorderLayout.NORTH); + } + + /** + * Wraps the content pane of the dialog with this instance + * + * @param dialog + * a fully configured dialog + */ + public void addToDialog(JDialog dialog) { + if (!(dialog.getContentPane() instanceof DeprecationWarning) && originalContentPane == null) { + originalContentPane = dialog.getContentPane(); + add(originalContentPane, BorderLayout.CENTER); + dialog.setContentPane(this); + } + } + + /** + * Restores the previous content pane of the dialog + * + * @param dialog + * the same dialog that was used for {@link #addToDialog(JDialog)} + */ + public void removeFromDialog(JDialog dialog) { + Container contentPane = originalContentPane; + if (equals(dialog.getContentPane()) && contentPane != null) { + dialog.setContentPane(contentPane); + originalContentPane = null; + } + } + + /** + * Creates a multiline text label, which does not support HTML + * + * @param text + * the text of the label + * @return a multi line label + */ + private static JComponent createMultiLineLabel(String text) { + JTextArea flowLabel = new JTextArea(text, 1, 0); + flowLabel.setDisabledTextColor(flowLabel.getForeground()); + flowLabel.setMinimumSize(new Dimension(20, 40)); + flowLabel.setBackground(null); + flowLabel.setLineWrap(true); + flowLabel.setWrapStyleWord(true); + flowLabel.setBorder(BorderFactory.createEmptyBorder()); + flowLabel.setOpaque(false); + flowLabel.setEnabled(false); + return flowLabel; + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/components/InjectableComponentWrapper.java b/src/main/java/com/rapidminer/connection/gui/components/InjectableComponentWrapper.java new file mode 100644 index 000000000..6538a7be2 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/components/InjectableComponentWrapper.java @@ -0,0 +1,74 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.components; + +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; + +import com.rapidminer.connection.gui.model.ConnectionParameterModel; + + +/** + * A wrapper for injectable component, displays {@link InjectedParameterPlaceholderLabel} instead of the component if the parameter is injected + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public class InjectableComponentWrapper extends JPanel { + + /** + * Creates a {@link InjectableComponentWrapper} + * + * @param component the component to wrap + * @param parameter the parameter which is displayed by the {@code component} + */ + public InjectableComponentWrapper(JComponent component, ConnectionParameterModel parameter) { + super(new GridBagLayout()); + JComponent placeholder = new InjectedParameterPlaceholderLabel(parameter); + component.setVisible(!parameter.isInjected()); + placeholder.setVisible(parameter.isInjected()); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.weightx = 1.0; + gbc.fill = GridBagConstraints.HORIZONTAL; + add(component, gbc); + add(placeholder, gbc); + parameter.injectorNameProperty().addListener((observable, oldValue, newValue) -> SwingUtilities.invokeLater(() -> { + if (component.isVisible()) { + Dimension size = new Dimension(Math.max(placeholder.getWidth(), component.getWidth()), + Math.max(placeholder.getHeight(), component.getHeight())); + placeholder.setPreferredSize(size); + placeholder.setMinimumSize(size); + } + component.setVisible(!parameter.isInjected()); + placeholder.setVisible(parameter.isInjected()); + revalidate(); + repaint(); + })); + parameter.enabledProperty().addListener((observable, oldValue, newValue) -> placeholder.setEnabled(newValue)); + placeholder.setEnabled(parameter.isEnabled()); + } + +} diff --git a/src/main/java/com/rapidminer/connection/gui/components/InjectedParameterPlaceholderLabel.java b/src/main/java/com/rapidminer/connection/gui/components/InjectedParameterPlaceholderLabel.java new file mode 100644 index 000000000..84c3073ae --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/components/InjectedParameterPlaceholderLabel.java @@ -0,0 +1,150 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.components; + +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import javax.swing.ImageIcon; +import javax.swing.JEditorPane; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.event.HyperlinkEvent; +import javax.swing.text.html.HTMLDocument; + +import com.rapidminer.connection.gui.ValueProviderGUIProvider; +import com.rapidminer.connection.gui.ValueProviderGUIRegistry; +import com.rapidminer.connection.gui.model.ConnectionModelConverter; +import com.rapidminer.connection.gui.model.ConnectionParameterModel; +import com.rapidminer.connection.gui.model.ValueProviderModel; +import com.rapidminer.connection.gui.model.ValueProviderParameterModel; +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.gui.tools.ExtendedHTMLJEditorPane; +import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.tools.RMUrlHandler; + +import javafx.beans.value.ChangeListener; +import javafx.collections.ListChangeListener; + + +/** + * Placeholder label that should be shown instead of an injected parameter. + * + * @author Jonas Wilms-Pfau + * @since 9.3.0 + */ +public class InjectedParameterPlaceholderLabel extends JPanel { + + private static final String INJECTED_PARAMETER = ConnectionI18N.getConnectionGUILabel("injected_parameter"); + private static final ImageIcon INJECTED_ICON = SwingTools.createIcon(ConnectionI18N.getConnectionGUIMessage("injected_parameter.icon")); + + private final JEditorPane editorPane = new ExtendedHTMLJEditorPane("text/html", INJECTED_PARAMETER); + + private final transient ChangeListener valueProviderChanged; + private final transient ListChangeListener valueProviderParameterChanged; + + public InjectedParameterPlaceholderLabel(ConnectionParameterModel param) { + super(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + JLabel icon = new JLabel("", INJECTED_ICON, JLabel.LEFT); + add(icon, gbc); + Font font = icon.getFont(); + String bodyRule = "body { font-family: " + font.getFamily() + "; " + + "font-size: " + font.getSize() + "pt; }"; + ((HTMLDocument) editorPane.getDocument()).getStyleSheet().addRule(bodyRule); + editorPane.setOpaque(false); + editorPane.setBorder(null); + editorPane.setEditable(false); + editorPane.addHyperlinkListener(event -> { + if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED && event.getURL() != null) { + RMUrlHandler.openInBrowser(event.getURL()); + } + }); + gbc.insets.left = icon.getIconTextGap(); + gbc.weightx = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + add(editorPane, gbc); + + valueProviderChanged = (i, o, n) -> updateText(param); + valueProviderParameterChanged = c -> { + while (c.next()) { + for (ValueProviderParameterModel valueProviderParameterModel : c.getAddedSubList()) { + valueProviderParameterModel.valueProperty().removeListener(valueProviderChanged); + valueProviderParameterModel.valueProperty().addListener(valueProviderChanged); + } + } + updateText(param); + }; + param.injectorNameProperty().addListener((i, o, n) -> registerListener(param)); + registerListener(param); + } + + /** + * Registers listener on the Value Provider + * + * @param param + * the connection parameter model + */ + private void registerListener(ConnectionParameterModel param) { + ValueProviderModel valueProvider = param.getValueProvider(); + if (valueProvider == null) { + editorPane.setText(INJECTED_PARAMETER); + return; + } + valueProvider.parametersProperty().removeListener(valueProviderParameterChanged); + valueProvider.parametersProperty().addListener(valueProviderParameterChanged); + + for (ValueProviderParameterModel parameter : valueProvider.parametersProperty()) { + parameter.valueProperty().removeListener(valueProviderChanged); + parameter.valueProperty().addListener(valueProviderChanged); + } + + updateText(param); + } + + /** + * Updates the text of the editorPane if the value provider is updated + * + * @param param + * the parameter model + */ + private void updateText(ConnectionParameterModel param) { + ValueProviderModel valueProvider = param.getValueProvider(); + + if (valueProvider == null) { + return; + } + + ValueProviderGUIProvider guiProvider = ValueProviderGUIRegistry.INSTANCE.getGUIProvider(valueProvider.getType()); + String hint = guiProvider == null ? null : guiProvider.getCustomLabel(ValueProviderGUIProvider.CustomLabel.INJECTED_PARAMETER, valueProvider, ConnectionModelConverter.getConnection(param), param.getGroupName(), param.getName()); + + if (hint != null) { + editorPane.setText(hint); + return; + } + + String fallback = ConnectionI18N.getConnectionGUIMessageOrNull("valueprovider.hint.injected_parameter_template.label", valueProvider.getName()); + if (fallback != null) { + editorPane.setText(fallback.trim()); + } else { + editorPane.setText(INJECTED_PARAMETER); + } + } + +} diff --git a/src/main/java/com/rapidminer/connection/gui/components/InjectionPanel.java b/src/main/java/com/rapidminer/connection/gui/components/InjectionPanel.java new file mode 100644 index 000000000..09dad8247 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/components/InjectionPanel.java @@ -0,0 +1,75 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.components; + +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.Window; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import com.rapidminer.connection.gui.actions.InjectParametersAction; +import com.rapidminer.connection.gui.model.ConnectionParameterModel; +import com.rapidminer.connection.valueprovider.ValueProvider; +import com.rapidminer.tools.I18N; + + +/** + * Injection Panel, contains out of a "Inject Parameter" button and a label + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public class InjectionPanel extends JPanel{ + + /** + * Creates a new injection panel + * + * @param parent + * the parent window + * @param type + * the connection type + * @param injectableParameters + * injectable parameter supplier + * @param valueProviders + * names of the available value providers + */ + public InjectionPanel(Window parent, String type, Supplier> injectableParameters, List valueProviders, Consumer> saveCallback){ + super(new GridBagLayout()); + + GridBagConstraints c = new GridBagConstraints(); + c.insets = new Insets(8, 10, 8, 16); + c.anchor = GridBagConstraints.WEST; + c.fill = GridBagConstraints.NONE; + JLabel injectLabel = new JLabel(I18N.getGUIMessage("gui.action.inject_connection_parameter.tip")); + injectLabel.setHorizontalAlignment(JLabel.LEFT); + // Copy parameters + JButton injectButton = new JButton(new InjectParametersAction(parent, type, injectableParameters, valueProviders, saveCallback)); + injectLabel.setLabelFor(injectButton); + add(injectButton, c); + + c.weightx = 1; + add(injectLabel, c); + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/components/TestConnectionPanel.java b/src/main/java/com/rapidminer/connection/gui/components/TestConnectionPanel.java new file mode 100644 index 000000000..747f0d487 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/components/TestConnectionPanel.java @@ -0,0 +1,215 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.components; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.geom.Rectangle2D; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.swing.BorderFactory; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.ScrollPaneConstants; + +import com.rapidminer.connection.ConnectionHandlerRegistry; +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.ConnectionStatistics; +import com.rapidminer.connection.gui.actions.TestConnectionAction; +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.connection.util.TestResult; +import com.rapidminer.gui.actions.CopyStringToClipboardAction; +import com.rapidminer.gui.look.Colors; +import com.rapidminer.gui.tools.ExtendedJScrollPane; +import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.gui.tools.components.FixedWidthLabel; + + +/** + * A panel that allows to test a {@link ConnectionInformation} and show the result. + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public class TestConnectionPanel extends JPanel { + + private static final Color TEST_BACKGROUND = Colors.PANEL_BACKGROUND; + private static final Color TEST_BORDER = Colors.TAB_BORDER; + + private static final ImageIcon RUNNING_ICON = SwingTools.createIcon("16/" + ConnectionI18N.getConnectionGUIMessage("test.running.icon")); + private static final ImageIcon EMPTY_ICON = SwingTools.createIconFromColor(TEST_BACKGROUND, TEST_BACKGROUND, 16, 16, new Rectangle2D.Double(0, 0, 16, 16)); + private static final ImageIcon NOT_SUPPORTED_ICON = SwingTools.createIcon("16/" + ConnectionI18N.getConnectionGUIMessage("test.not_supported.icon")); + private static final ImageIcon WARNING_ICON = SwingTools.createIcon("16/" + ConnectionI18N.getConnectionGUIMessage("test.warning.icon")); + private static final ImageIcon SUCCESS_ICON = SwingTools.createIcon("16/" + ConnectionI18N.getConnectionGUIMessage("test.success.icon")); + private static final Color COLOR_SUCCESS = new Color(75, 120, 0); + private static final String NEWLINE_HTML = "
"; + private static final String NEWLINE_TEXT = "\n"; + + private final JLabel resultIconLabel; + private final FixedWidthLabel resultDisplay; + private final transient ConnectionStatistics statistics; + private final String connectionType; + + + /** + * Creates a new test connection panel + * + * @param type the connection type + * @param connectionSupplier + * the supplier for the connection + * @param testResultConsumer + * the test result consumer that should be told about test results + * @param statistics + * the statistics that get updated with test results + */ + public TestConnectionPanel(String type, Supplier connectionSupplier, Consumer testResultConsumer, ConnectionStatistics statistics) { + super(new GridBagLayout()); + this.statistics = statistics; + this.connectionType = type; + boolean isTypeKnown = ConnectionHandlerRegistry.getInstance().isTypeKnown(type); + setPreferredSize(new Dimension(431, 60)); + GridBagConstraints gbc = new GridBagConstraints(); + setBackground(TEST_BACKGROUND); + setBorder(BorderFactory.createLineBorder(TEST_BORDER)); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.anchor = GridBagConstraints.WEST; + gbc.insets = new Insets(10, 10, 10, 10); + TestConnectionAction testAction = new TestConnectionAction(connectionSupplier, testResultConsumer); + testAction.setEnabled(isTypeKnown); + testAction.addPropertyChangeListener(evt -> repaint()); + JButton testButton = new JButton(testAction); + add(testButton, gbc); + + gbc.gridx = 1; + gbc.insets = new Insets(0, 0, 0, 10); + resultIconLabel = new JLabel(); + resultIconLabel.setIcon(EMPTY_ICON); + add(resultIconLabel, gbc); + + gbc.gridx = 2; + gbc.insets = new Insets(0, 5, 0, 0); + gbc.fill = GridBagConstraints.BOTH; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + resultDisplay = new FixedWidthLabel(300, ""); + resultDisplay.setBackground(TEST_BACKGROUND); + JPopupMenu copyMenu = new JPopupMenu(); + copyMenu.add(new CopyStringToClipboardAction(true, "connection.test.copy_result", TestConnectionPanel.this::getTestResultText)); + resultDisplay.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + handlePopup(e); + } + + @Override + public void mousePressed(MouseEvent e) { + handlePopup(e); + } + + @Override + public void mouseReleased(MouseEvent e) { + handlePopup(e); + } + + private void handlePopup(MouseEvent e) { + if (e.isPopupTrigger()) { + copyMenu.show(resultDisplay, e.getX(), e.getY()); + } + } + }); + JScrollPane scrollPane = new ExtendedJScrollPane(resultDisplay); + + if (!isTypeKnown) { + setTestResult(new TestResult(TestResult.ResultType.FAILURE, "unknown_provider_warning.label", null)); + } + + + scrollPane.getViewport().setBackground(TEST_BACKGROUND); + scrollPane.setBorder(BorderFactory.createEmptyBorder()); + scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + add(scrollPane, gbc); + } + + /** + * Updates the test result + * + * @param testResult + * the test result + */ + public void setTestResult(TestResult testResult) { + if (testResult == null) { + resultDisplay.setText(""); + resultIconLabel.setIcon(EMPTY_ICON); + return; + } + + // prepare proper i18n message + String i18nMessage; + if (!testResult.getParameterErrorMessages().isEmpty()) { + StringBuilder sb = new StringBuilder(); + testResult.getParameterErrorMessages().forEach((key, i18nKey) -> { + // we can rely on this split because it's enforced in the val result builder + String[] splittedGroupKey = key.split("\\.", 2); + String groupKey = splittedGroupKey[0]; + String parameterKey = splittedGroupKey[1]; + sb.append(ConnectionI18N.getValidationErrorMessage(i18nKey, connectionType, groupKey, parameterKey)).append(NEWLINE_HTML); + }); + i18nMessage = sb.toString(); + } else { + i18nMessage = ConnectionI18N.getConnectionGUIMessage(testResult.getMessageKey(), testResult.getArguments()); + } + + switch (testResult.getType()) { + case SUCCESS: + statistics.updateSuccess(); + resultDisplay.setForeground(COLOR_SUCCESS); + resultIconLabel.setIcon(SUCCESS_ICON); + break; + case FAILURE: + statistics.updateError(i18nMessage); + resultDisplay.setForeground(Color.RED); + resultIconLabel.setIcon(WARNING_ICON); + break; + case NOT_SUPPORTED: + resultDisplay.setForeground(Color.BLACK); + resultIconLabel.setIcon(NOT_SUPPORTED_ICON); + break; + case NONE: + default: + resultDisplay.setForeground(Color.BLACK); + resultIconLabel.setIcon(RUNNING_ICON); + } + + resultDisplay.setText(i18nMessage); + } + + private String getTestResultText() { + return resultDisplay.getPlaintext().replaceAll(NEWLINE_HTML, NEWLINE_TEXT); + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/dto/ConnectionInformationHolder.java b/src/main/java/com/rapidminer/connection/gui/dto/ConnectionInformationHolder.java new file mode 100644 index 000000000..517845a17 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/dto/ConnectionInformationHolder.java @@ -0,0 +1,189 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.dto; + +import java.io.IOException; + +import com.rapidminer.connection.ConnectionHandlerRegistry; +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.ConnectionInformationBuilder; +import com.rapidminer.connection.ConnectionInformationContainerIOObject; +import com.rapidminer.connection.configuration.ConnectionConfigurationBuilder; +import com.rapidminer.repository.Repository; +import com.rapidminer.tools.ValidationUtil; +import com.rapidminer.repository.ConnectionEntry; +import com.rapidminer.repository.RepositoryException; +import com.rapidminer.repository.RepositoryLocation; +import com.rapidminer.tools.PasswordInputCanceledException; + + +/** + * Wrapper object containing the connection information and its location. + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public final class ConnectionInformationHolder { + + private final RepositoryLocation location; + private final ConnectionInformation connection; + private final ConnectionInformation original; + private final boolean isEditable; + private final String connectionType; + + /** + * Creates a new ConnectionInformationHolder + * + * @param connection + * the connection + * @param location + * the location of the connection + * @param isEditable + * if the connection is editable + * @throws IOException + * in case the connection could not be copied + */ + private ConnectionInformationHolder(ConnectionInformation connection, RepositoryLocation location, boolean isEditable) throws IOException { + this(connection, null, location, isEditable); + } + + /** + * Creates a new ConnectionInformationHolder + * + * @param connection + * the connection + * @param name + * the name of the connection + * @param location + * the location of the connection + * @param isEditable + * if the connection is editable + * @throws IOException + * in case the connection could not be copied + */ + private ConnectionInformationHolder(ConnectionInformation connection, String name, RepositoryLocation location, boolean isEditable) throws IOException { + ValidationUtil.requireNonNull(connection, "connection"); + ValidationUtil.requireNonNull(location, "repository location"); + this.location = location; + + ConnectionInformationBuilder builder; + if (name != null) { + builder = new ConnectionInformationBuilder(connection) + .updateConnectionConfiguration(new ConnectionConfigurationBuilder(connection.getConfiguration(), name).build()); + } else { + builder = new ConnectionInformationBuilder(connection); + } + try { + // set repo for conn info + Repository repository = location.getRepository(); + builder.inRepository(repository); + connection = new ConnectionInformationBuilder(connection).inRepository(repository).build(); + } catch (RepositoryException e) { + // ignore + } + this.connection = connection; + this.original = builder.build(); + this.isEditable = isEditable; + this.connectionType = connection.getConfiguration().getType(); + } + + /** + * @return the stored connection information + */ + public ConnectionInformation getConnectionInformation() { + return connection; + } + + /** + * @return the location of the connection + */ + public RepositoryLocation getLocation() { + return location; + } + + /** + * @return {@code true} if the connection has changed + */ + public boolean hasChanged(ConnectionInformation connection) { + return !original.equals(connection); + } + + /** + * @return {@code true} if the connection is editable + */ + public boolean isEditable() { + return isEditable; + } + + /** + * @return the connection type + */ + public String getConnectionType() { + return connectionType; + } + + /** + * Creates a new ConnectionInformationHolder from a {@link ConnectionEntry} + * + * @param entry + * The connection entry + * @return a newly created ConnectionInformationHolder + * @throws RepositoryException + * in case the retrieval of the connection failed + * @throws IOException + * in case the connection could not be copied + */ + public static ConnectionInformationHolder from(ConnectionEntry entry) throws RepositoryException, IOException, PasswordInputCanceledException { + return new ConnectionInformationHolder(((ConnectionInformationContainerIOObject) entry.retrieveData(null)) + .getConnectionInformation(), entry.getLocation(), entry.isEditable()); + } + + /** + * Creates a new ConnectionInformationHolder from an existing {@link ConnectionInformation}. + * + * @param connection + * the connection to base the holder on + * @param name + * the name of the connection + * @param location + * the location of the connection + */ + public static ConnectionInformationHolder from(ConnectionInformation connection, String name, RepositoryLocation location) throws IOException { + return new ConnectionInformationHolder(connection, name, location, true); + } + + /** + * Creates a new ConnectionInformation of the given name and type + * + * @param name + * the name of the connection + * @param type + * the type of the connection + * @param location + * the future location of the connection + * @return a new holder containing an empty connection object and the location + * @throws IOException + * in case the connection could not be copied + */ + public static ConnectionInformationHolder createNewConnection(String name, String type, RepositoryLocation location) throws IOException { + ConnectionInformation connection = ConnectionHandlerRegistry.getInstance().getHandler(type).createNewConnectionInformation(name); + return new ConnectionInformationHolder(connection, name, location, true); + } + +} diff --git a/src/main/java/com/rapidminer/connection/gui/listener/TextChangedDocumentListener.java b/src/main/java/com/rapidminer/connection/gui/listener/TextChangedDocumentListener.java new file mode 100644 index 000000000..92302bc96 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/listener/TextChangedDocumentListener.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.listener; + +import java.util.logging.Level; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; + +import com.rapidminer.tools.LogService; + +import javafx.beans.property.Property; + + +/** + * Listener that calls the given Consumer every time the documents changes + * + * @author Jonas Wilms-Pfau + * @since 9.3.0 + */ +public class TextChangedDocumentListener implements DocumentListener { + + private final Property textProperty; + + public TextChangedDocumentListener(Property textProperty) { + this.textProperty = textProperty; + } + + @Override + public void insertUpdate(DocumentEvent e) { + updateText(e); + } + + @Override + public void removeUpdate(DocumentEvent e) { + updateText(e); + } + + @Override + public void changedUpdate(DocumentEvent e) { + updateText(e); + } + + private void updateText(DocumentEvent e) { + Document document = e.getDocument(); + try { + String newValue = document.getText(0, document.getLength()); + if (!newValue.equals(textProperty.getValue())) { + textProperty.setValue(newValue); + } + } catch (BadLocationException ble) { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.connection.gui.model.TextChangedDocumentListener.document_get_text_failed", ble); + } + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/model/ConnectionModel.java b/src/main/java/com/rapidminer/connection/gui/model/ConnectionModel.java new file mode 100644 index 000000000..58ae5d0db --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/model/ConnectionModel.java @@ -0,0 +1,413 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.model; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.connection.valueprovider.ValueProvider; +import com.rapidminer.connection.valueprovider.handler.ValueProviderHandlerRegistry; +import com.rapidminer.repository.RepositoryException; +import com.rapidminer.repository.RepositoryLocation; +import com.rapidminer.repository.internal.remote.RemoteRepository; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + + +/** + * ConnectionModel backing connection UIs. It contains all relevant pieces of a {@link + * com.rapidminer.connection.ConnectionInformation ConnectionInformation} object. + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public class ConnectionModel { + + private final RepositoryLocation location; + private final boolean editable; + private final StringProperty description = new SimpleStringProperty(); + private final ObservableList tags = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + private final ObservableList parameterGroups = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + private final ObservableList valueProviders = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + private final ObservableList placeholders = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + private final ObservableList libraryFiles = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + private final ObservableList otherFiles = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + private final ConnectionInformation information; + + + /** + * Copy constructor + * + *

This copy constructor does only copy the data, not the listeners!

+ * + * @param model + * the connection model to copy + */ + ConnectionModel(ConnectionModel model) { + this.information = model.information; + this.location = model.location; + this.editable = model.editable; + this.description.setValue(model.description.getValue()); + this.tags.addAll(model.tags); + + for (ConnectionParameterGroupModel group : model.parameterGroups) { + this.parameterGroups.add(new ConnectionParameterGroupModel(this, group)); + } + + for (ValueProviderModel vp : model.valueProviders) { + this.valueProviders.add(new ValueProviderModel(vp)); + } + + for (PlaceholderParameterModel placeholder : model.placeholders) { + this.placeholders.add(new PlaceholderParameterModel(this, placeholder)); + } + + this.libraryFiles.addAll(model.libraryFiles); + this.otherFiles.addAll(model.otherFiles); + } + + ConnectionModel(ConnectionInformation information, RepositoryLocation location, boolean editable, List valueProviders) { + this.information = information; + this.location = location; + this.editable = editable; + this.valueProviders.setAll(valueProviders); + } + + public RepositoryLocation getLocation() { + return location; + } + + public String getType() { + return information.getConfiguration().getType(); + } + + public String getName() { + return information.getConfiguration().getName(); + } + + public String getDescription() { + return description.get(); + } + + public StringProperty descriptionProperty() { + return description; + } + + public void setDescription(String description) { + this.description.setValue(description); + } + + public ObservableList getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags.setAll(tags); + } + + /** + * @return {@code true} if the connection is editable + */ + public boolean isEditable() { + return editable; + } + + /** + * Use {@link #removeParameterGroup(String)} or {@link #addOrSetParameter} and {@link #removeParameter} to add and + * remove placeholders + * + * @return unmodifiable observable list + */ + public ObservableList getParameterGroups() { + return FXCollections.unmodifiableObservableList(parameterGroups); + } + + /** + * Gets a parameter group by its name + * + * @param groupName + * the parameter group name + * @return the parameter group for this name, or {@code null} + */ + public ConnectionParameterGroupModel getParameterGroup(String groupName) { + for (ConnectionParameterGroupModel group : parameterGroups) { + if (group.getName().equals(groupName)) { + return group; + } + } + return null; + } + + /** + * Removes a parameter group + * + * @param groupName + * the parameter group name + * @return {@code true} if a group with this name was removed + */ + public boolean removeParameterGroup(String groupName) { + ConnectionParameterGroupModel group = getParameterGroup(groupName); + if (group == null) { + return false; + } + synchronized (parameterGroups) { + return parameterGroups.remove(group); + } + } + + /** + * Gets or creates the parameter group + * + * @param groupName + * the group name + * @return the existing group of this name or a new one + */ + public ConnectionParameterGroupModel getOrCreateParameterGroup(String groupName) { + ConnectionParameterGroupModel model = getParameterGroup(groupName); + + if (model != null) { + return model; + } + + synchronized (parameterGroups) { + model = getParameterGroup(groupName); + if (model == null) { + model = new ConnectionParameterGroupModel(this, groupName); + parameterGroups.add(model); + } + } + + return model; + } + + /** + * Gets a parameter + * + * @param groupName + * the group name of the parameter + * @param parameterName + * the name of the parameter + * @return the parameter, or {@code null} + */ + public ConnectionParameterModel getParameter(String groupName, String parameterName) { + return Optional.ofNullable(getParameterGroup(groupName)).map(m -> m.getParameter(parameterName)).orElse(null); + } + + /** + * Use {@link #addOrSetPlaceholder} and {@link #removePlaceholder} to add and remove placeholders + * + * @return unmodifiable observable list + */ + public ObservableList getPlaceholders() { + return FXCollections.unmodifiableObservableList(placeholders); + } + + /** + * Gets a placeholder + * + * @param groupName + * the group name of the parameter + * @param parameterName + * the name of the parameter + * @return the placeholder, or {@code null} + */ + public PlaceholderParameterModel getPlaceholder(String groupName, String parameterName) { + for (PlaceholderParameterModel placeholder : placeholders) { + if (placeholder.getGroupName().equals(groupName) && placeholder.getName().equals(parameterName)) { + return placeholder; + } + } + return null; + } + + /** + * Adds a placeholder + * + * @param groupName + * the group name + * @param name + * the parameter name + * @param isEncrypted + * if the parameter is encrypted + * @param injectorName + * the name of the value provider + * @param isEnabled + * if the parameter is enabled + * @return {@code true} if the parameter was added + */ + public boolean addOrSetPlaceholder(String groupName, String name, String value, boolean isEncrypted, String injectorName, boolean isEnabled) { + PlaceholderParameterModel parameter = getPlaceholder(groupName, name); + if (parameter != null) { + parameter.setValue(value); + parameter.setEncrypted(isEncrypted); + parameter.setInjectorName(injectorName); + parameter.setEnabled(isEnabled); + return false; + } + return placeholders.add(new PlaceholderParameterModel(this, groupName, name, value, isEncrypted, injectorName, isEnabled)); + } + + /** + * Removes placeholder + * + * @param groupName + * the group name + * @param parameterName + * the parameter name + * @return {@code true} if the parameter was removed + */ + public boolean removePlaceholder(String groupName, String parameterName) { + return placeholders.removeIf(param -> param.getGroupName().equals(groupName) && param.getName().equals(parameterName)); + } + + /** + * Adds a parameter + * + * @param groupName + * the group name + * @param name + * the parameter name + * @param isEncrypted + * if the parameter is encrypted + * @param injectorName + * the name of the value provider + * @param isEnabled + * if the parameter is enabled + * @return {@code true} if the parameter was added + */ + public boolean addOrSetParameter(String groupName, String name, String value, boolean isEncrypted, String injectorName, boolean isEnabled) { + return getOrCreateParameterGroup(groupName).addOrSetParameter(name, value, isEncrypted, injectorName, isEnabled); + } + + /** + * Removes a parameter + * + * @param groupName + * the group name + * @param parameterName + * the parameter name + * @return {@code true} if the parameter was removed + */ + public boolean removeParameter(String groupName, String parameterName) { + return Optional.ofNullable(getParameterGroup(groupName)) + .map(g -> g.removeParameter(parameterName)).orElse(false); + } + + /** + * @return a shallow copy of {@link #valueProvidersProperty()} + */ + public List getValueProviders() { + return new ArrayList<>(valueProvidersProperty()); + } + + /** + * If not editing the list of used value providers will be shown. If editing all possible value providers need to be + * shown to be able to configure them. + * + * @return mutable observable list + */ + public ObservableList valueProvidersProperty() { + if (editable) { + setupOtherValueProviders(); + } + return valueProviders; + } + + /** + * Replaces the current value providers with the given ones + * + * @param valueProviders + * the new value providers + */ + public void setValueProviders(List valueProviders) { + this.valueProviders.setAll(valueProviders); + } + + /** + * @return mutable observable list of library files + */ + public ObservableList getLibraryFiles() { + return libraryFiles; + } + + public void setLibraryFiles(List libraryFiles) { + this.libraryFiles.setAll(libraryFiles); + } + + /** + * @return mutable observable list of other files + */ + public ObservableList getOtherFiles() { + return otherFiles; + } + + public void setOtherFiles(List otherFiles) { + this.otherFiles.setAll(otherFiles); + } + + /** + * Creates a copy of the data without the listeners + * + * @return a copy without the listeners + */ + public ConnectionModel copyDataOnly() { + return new ConnectionModel(this); + } + + /** + * Add those {@link ValueProvider ValueProviders} to the valueprovider list that were not used so far by this + * connection. These need to be configurable in editmode and therefore exist in the list. They will be added in + * alphabetical order. + */ + private void setupOtherValueProviders() { + String filter; + try { + filter = location.getRepository() instanceof RemoteRepository ? ValueProviderHandlerRegistry.REMOTE : null; + } catch (RepositoryException e) { + filter = ValueProviderHandlerRegistry.REMOTE; + } + final List newValueProviders = ValueProviderHandlerRegistry.getInstance().getVisibleTypes(filter) + .stream().filter(aType -> this.valueProviders.stream().noneMatch(vp -> aType.equals(vp.getType()))) + .map(type -> ValueProviderHandlerRegistry.getInstance().getHandler(type).createNewProvider(ConnectionI18N.getValueProviderTypeName(type))) + .map(ValueProviderModelConverter::toModel).collect(Collectors.toList()); + if (newValueProviders.isEmpty()) { + return; + } + newValueProviders.sort(Comparator.comparing(ValueProviderModel::getName)); + valueProviders.addAll(newValueProviders); + } + + /** + * Converts the current configuration into an ConnectionInformation object + * + * @return a ConnectionInformation object which contains the current information + */ + ConnectionInformation asConnectionInformation() { + return ConnectionModelConverter.applyConnectionModel(information, this); + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/model/ConnectionModelConverter.java b/src/main/java/com/rapidminer/connection/gui/model/ConnectionModelConverter.java new file mode 100644 index 000000000..edde17c83 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/model/ConnectionModelConverter.java @@ -0,0 +1,218 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.model; + + +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.stream.Collectors; + +import org.apache.commons.lang.StringUtils; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.ConnectionInformationBuilder; +import com.rapidminer.connection.configuration.ConfigurationParameter; +import com.rapidminer.connection.configuration.ConfigurationParameterGroup; +import com.rapidminer.connection.configuration.ConfigurationParameterImpl; +import com.rapidminer.connection.configuration.ConnectionConfigurationBuilder; +import com.rapidminer.connection.configuration.PlaceholderParameter; +import com.rapidminer.connection.configuration.PlaceholderParameterImpl; +import com.rapidminer.connection.valueprovider.ValueProvider; +import com.rapidminer.repository.RepositoryLocation; +import com.rapidminer.tools.LogService; + + +/** + * Utility class to convert between {@link ConnectionModel}s and {@link ConnectionInformation} objects. + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public final class ConnectionModelConverter { + + /** + * Prevent utility class instantiation. + */ + private ConnectionModelConverter() { + throw new AssertionError("Utility class"); + } + + /** + * Creates a ConnectionModel from a ConnectionInformation + * + * @param connection + * the {@link ConnectionInformation} object + * @param location + * the {@link RepositoryLocation} of the connection + * @param editable + * {@code true} if the connection is editable + * @return a connection model containing the information of the connection + */ + public static ConnectionModel fromConnection(ConnectionInformation connection, RepositoryLocation location, boolean editable) { + List valueProviderModels = connection.getConfiguration().getValueProviders().stream().map(ValueProviderModelConverter::toModel).collect(Collectors.toList()); + ConnectionModel conn = new ConnectionModel(connection, location, editable, valueProviderModels); + conn.setDescription(connection.getConfiguration().getDescription()); + conn.setTags(new ArrayList<>(connection.getConfiguration().getTags())); + for (ConfigurationParameterGroup group : connection.getConfiguration().getKeys()) { + ConnectionParameterGroupModel groupModel = conn.getOrCreateParameterGroup(group.getGroup()); + for (ConfigurationParameter p : group.getParameters()) { + groupModel.addOrSetParameter(p.getName(), p.getValue(), p.isEncrypted(), p.getInjectorName(), p.isEnabled()); + } + } + for (PlaceholderParameter p : connection.getConfiguration().getPlaceholders()) { + conn.addOrSetPlaceholder(p.getGroup(), p.getName(), p.getValue(), p.isEncrypted(), p.getInjectorName(), p.isEnabled()); + } + conn.setLibraryFiles(connection.getLibraryFiles()); + conn.setOtherFiles(connection.getOtherFiles()); + return conn; + } + + /** + * Applies the connection model on the connection + * + * @param connection + * the connection + * @param model + * the connection model + * @return a new connection with the updated information + */ + public static ConnectionInformation applyConnectionModel(ConnectionInformation connection, ConnectionModel model) { + ConnectionConfigurationBuilder config; + try { + // We have to clone to keep the id field + config = new ConnectionConfigurationBuilder(connection.getConfiguration()); + } catch (IOException e) { + LogService.getRoot().log(Level.SEVERE, "com.rapidminer.connection.gui.model.ConnectionModelConverter.cloning_connection_configuration_failed", e); + config = new ConnectionConfigurationBuilder(model.getType(), model.getName()); + } + config.withKeys(toMap(model.getParameterGroups())); + List valueProviders = model.valueProvidersProperty().stream() + .filter(vp -> model.getParameterGroups().stream() + .anyMatch(group -> group.getParameters().stream() + .anyMatch(param -> vp.getName().equals(param.getInjectorName())))).map(ValueProviderModelConverter::toValueProvider) + .collect(Collectors.toList()); + config.withValueProviders(valueProviders); + config.withTags(new ArrayList<>(model.getTags())); + config.withDescription(model.getDescription()); + config.withPlaceholders(toList(model.getPlaceholders())); + + ConnectionInformationBuilder connectionBuilder; + try { + connectionBuilder = new ConnectionInformationBuilder(connection).updateConnectionConfiguration(config.build()); + } catch (IOException e) { + LogService.getRoot().log(Level.SEVERE, "com.rapidminer.connection.gui.model.ConnectionModelConverter.cloning_connection_failed", e); + connectionBuilder = new ConnectionInformationBuilder(config.build()) + .withLibraryFiles(connection.getLibraryFiles()) + .withOtherFiles(connection.getOtherFiles()) + .withAnnotations(connection.getAnnotations()) + .withStatistics(connection.getStatistics()); + } + + try { + connectionBuilder.withOtherFiles(model.getOtherFiles()); + } catch (IllegalArgumentException e) { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.connection.gui.model.ConnectionModelConverter.removed_nonexisting_paths", e); + model.getOtherFiles().removeIf(Files::notExists); + connectionBuilder.withOtherFiles(model.getOtherFiles()); + } + + try { + connectionBuilder.withLibraryFiles(model.getLibraryFiles()); + } catch (IllegalArgumentException e) { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.connection.gui.model.ConnectionModelConverter.removed_nonexisting_paths", e); + model.getLibraryFiles().removeIf(Files::notExists); + connectionBuilder.withLibraryFiles(model.getLibraryFiles()); + } + + ConnectionInformation result = connectionBuilder.build(); + // We compare with the object that contains the updated statistics, since they don't modify the connection + if (!result.equals(connection)) { + result.getStatistics().updateChange(); + } + + return result; + } + + /** + * Returns a temporary configuration object for the parameter + * + * @param parameter + * the parameter + * @return a temporary configuration object + */ + public static ConnectionInformation getConnection(ConnectionParameterModel parameter) { + return parameter.getConnection().asConnectionInformation(); + } + + /** + * Returns a temporary configuration object for the model. + * + * @param model + * the connection model + * @return a temporary configuration object + */ + public static ConnectionInformation getConnection(ConnectionModel model) { + return model.asConnectionInformation(); + } + + /** + * Converts a list of group models into a Map of group name to configuration parameter + * + * @param groupModels + * the list of parameter group models + * @return a map of group name to list of configuration parameter + */ + static Map> toMap(List groupModels) { + LinkedHashMap> result = new LinkedHashMap<>(); + for (ConnectionParameterGroupModel group : groupModels) { + String groupName = group.getName(); + List parameters = new ArrayList<>(); + for (ConnectionParameterModel parameter : group.getParameters()) { + if (StringUtils.trimToNull(parameter.getName()) != null) { + parameters.add(new ConfigurationParameterImpl(parameter.getName(), parameter.getValue(), parameter.isEncrypted(), parameter.getInjectorName(), parameter.isEnabled())); + } + } + //We can't add empty groups + if (!parameters.isEmpty()) { + result.put(groupName, parameters); + } + } + return result; + } + + /** + * Converts a list of PlaceholderParameterModel into a list of AdvancedConfigurationParameter + * + * @param groupModels + * the parameter group models + * @return advanced configuration parameter + */ + static List toList(List groupModels) { + List result = new ArrayList<>(); + for (PlaceholderParameterModel parameter : groupModels) { + result.add(new PlaceholderParameterImpl(parameter.getName(), parameter.getValue(), parameter.getGroupName(), parameter.isEncrypted(), parameter.getInjectorName(), parameter.isEnabled())); + } + return result; + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/model/ConnectionParameterGroupModel.java b/src/main/java/com/rapidminer/connection/gui/model/ConnectionParameterGroupModel.java new file mode 100644 index 000000000..85b4fb031 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/model/ConnectionParameterGroupModel.java @@ -0,0 +1,152 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.model; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + + +/** + * ConnectionParameterGroupModel, converts to {@link com.rapidminer.connection.configuration.ConfigurationParameterGroup ConfigurationParameterGroup}. + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public class ConnectionParameterGroupModel { + + private final String name; + private final ObservableList parameters = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + private final ConnectionModel parent; + + /** + * Copy constructor + * + * @param parent + * the new parent connection + * @param group + * the original group + */ + ConnectionParameterGroupModel(ConnectionModel parent, ConnectionParameterGroupModel group) { + this.name = group.name; + this.parent = parent; + for (ConnectionParameterModel parameter : group.parameters) { + this.parameters.add(new ConnectionParameterModel(this, parameter)); + } + } + + /** + * Creates a new parameter group + * + * @param parent + * the new parent + * @param name + * the + */ + ConnectionParameterGroupModel(ConnectionModel parent, String name) { + this.parent = parent; + this.name = name; + } + + /** + * @return the name of the parameter group + */ + public String getName() { + return name; + } + + /** + * @return immutable observable list + */ + public ObservableList getParameters() { + return FXCollections.unmodifiableObservableList(parameters); + } + + /** + * Adds a parameter to this group + * + * @param name + * the name of the parameter + * @param value + * the value of the parameter + * @param isEncrypted + * if the parameter is encrypted + * @param injectorName + * the name of the value provider + * @param isEnabled + * if the parameter is enabled + * @return {@code true} in case the parameter was added, {@code false} if it was modified + */ + public synchronized boolean addOrSetParameter(String name, String value, boolean isEncrypted, String injectorName, boolean isEnabled) { + ConnectionParameterModel parameter = getParameter(name); + + if (parameter != null) { + parameter.setValue(value); + parameter.setEncrypted(isEncrypted); + parameter.setInjectorName(injectorName); + parameter.setEnabled(isEnabled); + return false; + } + + return parameters.add(new ConnectionParameterModel(this, name, value, isEncrypted, injectorName, isEnabled)); + } + + /** + * Gets a parameter by its name + * + * @param parameterName + * the parameter name + * @return the parameter for this name, or {@code null} + */ + public ConnectionParameterModel getParameter(String parameterName) { + for (ConnectionParameterModel parameter : parameters) { + if (parameter.getName().equals(parameterName)) { + return parameter; + } + } + return null; + } + + /** + * Removes a parameter + * + * @param parameterName + * the parameter name + * @return {@code true} if the parameter was removed + */ + public boolean removeParameter(String parameterName) { + return parameters.removeIf(p -> p.getName().equals(parameterName)); + } + + /** + * Creates a copy of the data without the listeners + * + * @return a copy without the listeners + */ + public ConnectionParameterGroupModel copyDataOnly() { + return new ConnectionParameterGroupModel(this.parent, this); + } + + /** + * @return the connection this parameter group belongs to + */ + ConnectionModel getParent() { + return parent; + } + +} diff --git a/src/main/java/com/rapidminer/connection/gui/model/ConnectionParameterModel.java b/src/main/java/com/rapidminer/connection/gui/model/ConnectionParameterModel.java new file mode 100644 index 000000000..2f529fbff --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/model/ConnectionParameterModel.java @@ -0,0 +1,228 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.model; + +import java.util.Objects; + +import org.apache.commons.lang.StringUtils; + +import com.rapidminer.connection.gui.components.InjectedParameterPlaceholderLabel; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + + +/** + * ConnectionParameterModel, maps to {@link com.rapidminer.connection.configuration.ConfigurationParameter}. + *

+ * Note that you can access the property objects, allowing you to register listeners for changes on each property where needed. + *

+ * + *

For components rendering this model, it's important to respect the encrypted, injected, and potentially (if needed) the enabled state + *

    + *
  • It is required to not display the value of an encrypted parameter in plain text!
  • + *
  • Injected Parameters should be identifiable, see {@link InjectedParameterPlaceholderLabel + * InjectedParameterPlaceholderLabel}
  • + *
+ *

+ * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public class ConnectionParameterModel { + + protected final StringProperty groupName = new SimpleStringProperty(); + private final StringProperty name = new SimpleStringProperty(); + private final StringProperty value = new SimpleStringProperty(); + private final BooleanProperty encrypted = new SimpleBooleanProperty(); + private final StringProperty injectorName = new SimpleStringProperty(); + private final BooleanProperty enabled = new SimpleBooleanProperty(); + private final StringProperty validationError = new SimpleStringProperty(); + private final ConnectionParameterGroupModel parent; + + + ConnectionParameterModel(ConnectionParameterGroupModel parent, String name, String value, boolean isEncrypted, String injectorName, boolean isEnabled) { + this.parent = parent; + this.groupName.set(parent.getName()); + this.name.set(name); + this.value.set(value); + this.encrypted.set(isEncrypted); + this.injectorName.set(injectorName); + this.enabled.set(isEnabled); + } + + /** + * Copy constructor. + * + * @param parameter + * the original which values will be copied + */ + ConnectionParameterModel(ConnectionParameterGroupModel parent, ConnectionParameterModel parameter) { + this.parent = parent; + this.groupName.set(parameter.groupName.get()); + this.name.set(parameter.name.get()); + this.value.set(parameter.value.get()); + this.encrypted.set(parameter.encrypted.get()); + this.injectorName.set(parameter.injectorName.get()); + this.enabled.set(parameter.isEnabled()); + } + + /** + * @return {@code true} if the parameter is editable; {@code false} otherwise. + */ + public boolean isEditable() { + return parent.getParent().isEditable(); + } + + /** + * @return the {@link ConnectionModel#getType()} + */ + public String getType() { + return parent.getParent().getType(); + } + + public String getGroupName() { + return groupName.get(); + } + + public String getName() { + return name.get(); + } + + public void setName(String name) { + this.name.set(name); + } + + public StringProperty nameProperty() { + return name; + } + + public String getValue() { + return value.get(); + } + + public void setValue(String value) { + this.value.set(value); + } + + public StringProperty valueProperty() { + return value; + } + + public boolean isEncrypted() { + return encrypted.get(); + } + + public void setEncrypted(boolean isEncrypted) { + this.encrypted.set(isEncrypted); + } + + public BooleanProperty encryptedProperty() { + return encrypted; + } + + /** + * Tests whether the injector name is not null and not empty. + * + * @return {@code true} if an inject is set; {@code false} otherwise + */ + public boolean isInjected() { + return StringUtils.trimToNull(injectorNameProperty().get()) != null; + } + + public String getInjectorName() { + return injectorName.get(); + } + + + /** + * @return the assigned value provider or {@code null} + */ + public ValueProviderModel getValueProvider() { + for (ValueProviderModel vp : getParent().getParent().valueProvidersProperty()) { + if (Objects.equals(getInjectorName(), vp.getName())) { + return vp; + } + } + return null; + } + + public void setInjectorName(String injectorName) { + this.injectorName.set(injectorName); + } + + public StringProperty injectorNameProperty() { + return injectorName; + } + + public String getValidationError() { + return validationError.get(); + } + + public void setValidationError(String validationError) { + this.validationError.set(validationError); + } + + public StringProperty validationErrorProperty() { + return validationError; + } + + /** + * See {@link com.rapidminer.connection.configuration.ConfigurationParameter#isEnabled()}. + * + * @return {@code true} if the parameter is enabled; {@code false} otherwise + */ + public boolean isEnabled() { + return enabled.get(); + } + + public void setEnabled(boolean enabled) { + this.enabled.set(enabled); + } + + public BooleanProperty enabledProperty() { + return enabled; + } + + /** + * Creates a copy of the data without the listeners + * + * @return a copy without the listeners + */ + public ConnectionParameterModel copyDataOnly() { + return new ConnectionParameterModel(getParent(), this); + } + + /** + * @return the group this parameter belongs to + */ + ConnectionParameterGroupModel getParent() { + return parent; + } + + /** + * @return the connection this parameter is part of + */ + ConnectionModel getConnection() { + return getParent().getParent(); + } + +} diff --git a/src/main/java/com/rapidminer/connection/gui/model/InjectParametersModel.java b/src/main/java/com/rapidminer/connection/gui/model/InjectParametersModel.java new file mode 100644 index 000000000..0390f2e30 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/model/InjectParametersModel.java @@ -0,0 +1,102 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.model; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import com.rapidminer.connection.valueprovider.ValueProvider; + + +/** + * Container for the data that is altered in the {@link com.rapidminer.connection.gui.InjectParametersDialog} + * + * @author Andreas Timm + * @since 9.3 + */ +public class InjectParametersModel { + + // the data to work with: + // original parameters are those initially set or actively changed and accepted in the UI + private final List originalParameters; + // parameters are being changed through the UI + private final List parameters; + // list of value providers to choose from + private final List valueProviders; + + /** + * Create an instance with some data. Will use empty lists in case one is missing. + * + * @param parameters + * the parameters to be edited + * @param valueProviders + * all available valueProviders to choose from + */ + public InjectParametersModel(List parameters, List valueProviders) { + this.originalParameters = parameters != null ? parameters.stream().map(ConnectionParameterModel::copyDataOnly).collect(Collectors.toList()) : Collections.emptyList(); + this.parameters = copyParameters(originalParameters); + this.valueProviders = valueProviders != null ? valueProviders : Collections.emptyList(); + } + + /** + * Create a copy of the given {@link ConnectionParameterModel} list + * + * @param copyMe + * list to be copied, + * @return a new list with new instances that are created using {@link ConnectionParameterModel#copyDataOnly()} + */ + private List copyParameters(List copyMe) { + return copyMe.stream().map(ConnectionParameterModel::copyDataOnly).collect(Collectors.toList()); + } + + /** + * The parameters to edit or an empty list. + * + * @return the parameters list to edit or an empty list, never {@code null} + */ + public List getParameters() { + return parameters; + } + + /** + * The available {@link ValueProvider ValueProviders} or an empty list. + * + * @return the available {@link ValueProvider ValueProviders} list or an empty list, never {@code null} + */ + public List getValueProviders() { + return valueProviders; + } + + /** + * Call me if changes to the parameters should be used + */ + public void setChangedParameters() { + originalParameters.clear(); + originalParameters.addAll(copyParameters(parameters)); + } + + /** + * Call me if changes to the parameters should be used + */ + public void resetChangedParameters() { + parameters.clear(); + parameters.addAll(copyParameters(originalParameters)); + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/model/PlaceholderParameterModel.java b/src/main/java/com/rapidminer/connection/gui/model/PlaceholderParameterModel.java new file mode 100644 index 000000000..3f5facde1 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/model/PlaceholderParameterModel.java @@ -0,0 +1,97 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.model; + +import com.rapidminer.connection.gui.components.InjectedParameterPlaceholderLabel; + +import javafx.beans.property.StringProperty; + + +/** + * PlaceholderParameterModel, maps to {@link com.rapidminer.connection.configuration.PlaceholderParameter} + * + *

For components rendering this model, it is important to respect the isEncrypted and isInjected state + *

    + *
  • It is required to not display the value of an encrypted parameter in plain text!
  • + *
  • Injected Parameters should be identifiable, see {@link InjectedParameterPlaceholderLabel InjectedParameterPlaceholderLabel}
  • + *
+ *

+ * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public class PlaceholderParameterModel extends ConnectionParameterModel { + + /** + * Copy constructor + * + * @param parent + * the new parent connection + * @param parameter + * the original parameter + */ + PlaceholderParameterModel(ConnectionModel parent, ConnectionParameterModel parameter) { + super(new ConnectionParameterGroupModel(parent, parameter.getGroupName()), parameter); + } + + /** + * Creates a new placeholder parameter + * + * @param parent + * the parent connection + * @param groupName + * the group name + * @param name + * the name of the parameter + * @param value + * the value of the parameter + * @param isEncrypted + * if the parameter is encrypted + * @param injectorName + * the name of the injector or {@code null} + * @param isEnabled + * if the parameter is enabled + */ + PlaceholderParameterModel(ConnectionModel parent, String groupName, String name, String value, boolean isEncrypted, String injectorName, boolean isEnabled) { + super(new ConnectionParameterGroupModel(parent, groupName), name, value, isEncrypted, injectorName, isEnabled); + } + + /** + * @return observable group name + */ + public StringProperty groupNameProperty() { + return groupName; + } + + /** + * Updates the group name + * + * @param groupName + * the new group name + */ + public void setGroupName(String groupName) { + this.groupName.set(groupName); + } + + @Override + public PlaceholderParameterModel copyDataOnly() { + return new PlaceholderParameterModel(getParent().getParent(), this); + } + +} diff --git a/src/main/java/com/rapidminer/connection/gui/model/ValueProviderModel.java b/src/main/java/com/rapidminer/connection/gui/model/ValueProviderModel.java new file mode 100644 index 000000000..5ce3a9316 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/model/ValueProviderModel.java @@ -0,0 +1,139 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.rapidminer.connection.valueprovider.ValueProvider; +import com.rapidminer.connection.valueprovider.ValueProviderParameter; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/** + * Observable {@link ValueProvider} implementation which is only used for the UI. + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public class ValueProviderModel implements ValueProvider { + + private final String type; + private final StringProperty name = new SimpleStringProperty(); + private final ObservableList parameters = FXCollections.observableArrayList(); + + /** + * Copy constructor + * + * @param valueProviderModel + * the original value provider model + */ + ValueProviderModel(ValueProvider valueProviderModel) { + this.type = valueProviderModel.getType(); + this.name.set(valueProviderModel.getName()); + for (ValueProviderParameter parameter : valueProviderModel.getParameters()) { + this.parameters.add(new ValueProviderParameterModel(parameter)); + } + } + + /** + * Creates a new ValueProviderModel + * + * @param name + * the name of the value provider + * @param type + * the type of the value provider + * @param parameters + * the parameters of the value provider + */ + ValueProviderModel(String name, String type, List parameters) { + this.type = type; + this.name.setValue(name); + this.parameters.setAll(parameters); + } + + @Override + public String getType() { + return type; + } + + @Override + public String getName() { + return name.get(); + } + + /** + * @return observable name property + */ + public StringProperty nameProperty() { + return name; + } + + /** + * Updates the name of the value provider + * + * @param name + * the new name of the value provider + */ + public void setName(String name) { + this.name.set(name); + } + + @Override + public List getParameters() { + return new ArrayList<>(parameters); + } + + /** + * @return observable parameters property + */ + public ObservableList parametersProperty() { + return parameters; + } + + @Override + public Map getParameterMap() { + return parameters.stream().collect(Collectors.toMap(ValueProviderParameter::getName, Function.identity())); + } + + /** + * Replaces the current parameters with the given ones + * + * @param parameters + * the new parameters + */ + public void setParameters(List parameters) { + this.parameters.setAll(parameters); + } + + /** + * Creates a copy of the data without the listeners + * + * @return a copy without the listeners + */ + public ValueProviderModel copyDataOnly() { + return new ValueProviderModel(this); + } +} diff --git a/src/main/java/com/rapidminer/connection/gui/model/ValueProviderModelConverter.java b/src/main/java/com/rapidminer/connection/gui/model/ValueProviderModelConverter.java new file mode 100644 index 000000000..224a5ae34 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/model/ValueProviderModelConverter.java @@ -0,0 +1,71 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.model; + +import java.util.ArrayList; +import java.util.List; + +import com.rapidminer.connection.valueprovider.ValueProvider; +import com.rapidminer.connection.valueprovider.ValueProviderImpl; +import com.rapidminer.connection.valueprovider.ValueProviderParameter; +import com.rapidminer.connection.valueprovider.ValueProviderParameterImpl; + + +/** + * Converts between {@link ValueProviderModel} and {@link ValueProviderImpl} + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public final class ValueProviderModelConverter { + + /** + * Prevent utility class instantiation. + */ + private ValueProviderModelConverter() { + throw new AssertionError("Utility class"); + } + + /** + * Converts a {@link ValueProviderImpl} into an {@link ValueProviderModel} + * + * @param valueProvider + * the {@link ValueProviderImpl} + * @return an observable {@link ValueProviderModel} + */ + static ValueProviderModel toModel(ValueProvider valueProvider) { + return new ValueProviderModel(valueProvider); + } + + /** + * Converts the given {@link ValueProviderModel} into a {@link ValueProviderImpl} + * + * @param model + * the observable model + * @return the {@link ValueProviderImpl} + */ + static ValueProviderImpl toValueProvider(ValueProvider model) { + List parameters = new ArrayList<>(); + for (ValueProviderParameter param : model.getParameters()) { + parameters.add(new ValueProviderParameterImpl(param.getName(), param.getValue(), param.isEncrypted(), param.isEnabled())); + } + return new ValueProviderImpl(model.getName(), model.getType(), parameters); + } + +} diff --git a/src/main/java/com/rapidminer/connection/gui/model/ValueProviderParameterModel.java b/src/main/java/com/rapidminer/connection/gui/model/ValueProviderParameterModel.java new file mode 100644 index 000000000..ae579b65d --- /dev/null +++ b/src/main/java/com/rapidminer/connection/gui/model/ValueProviderParameterModel.java @@ -0,0 +1,161 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.gui.model; + +import com.rapidminer.connection.valueprovider.ValueProviderParameter; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + + +/** + * Observable {@link ValueProviderParameter} implementation which is only used for the UI. + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public class ValueProviderParameterModel implements ValueProviderParameter { + + private final StringProperty name = new SimpleStringProperty(); + private final StringProperty value = new SimpleStringProperty(); + private final BooleanProperty encrypted = new SimpleBooleanProperty(true); + private final BooleanProperty enabled = new SimpleBooleanProperty(true); + + /** + * Copy constructor + * + * @param parameter + * the original parameter + */ + ValueProviderParameterModel(ValueProviderParameter parameter) { + this.name.set(parameter.getName()); + this.value.set(parameter.getValue()); + this.encrypted.set(parameter.isEncrypted()); + this.enabled.set(parameter.isEnabled()); + } + + /** + * Creates a new {@link ValueProviderParameterModel} + * + * @param name + * the name + * @param value + * the value + * @param encrypted + * if it is encrypted + * @param enabled + * if it is enabled + */ + ValueProviderParameterModel(String name, String value, boolean encrypted, boolean enabled) { + this.name.set(name); + this.value.set(value); + this.encrypted.set(encrypted); + this.enabled.set(enabled); + } + + + @Override + public String getName() { + return name.get(); + } + + /** + * @return the observable name property + */ + public StringProperty nameProperty() { + return name; + } + + /** + * Updates the parameter name + * + * @param name + * the new name + */ + public void setName(String name) { + this.name.set(name); + } + + @Override + public String getValue() { + return value.get(); + } + + /** + * @return the observable value property + */ + public StringProperty valueProperty() { + return value; + } + + @Override + public void setValue(String value) { + this.value.set(value); + } + + @Override + public boolean isEncrypted() { + return encrypted.get(); + } + + /** + * @return the observable encrypted property + */ + public BooleanProperty encryptedProperty() { + return encrypted; + } + + /** + * Updates the encrypted status of this property + * + * @param encrypted + * {@code true} if the property is encrypted + */ + public void setEncrypted(boolean encrypted) { + this.encrypted.set(encrypted); + } + + @Override + public boolean isEnabled() { + return enabled.get(); + } + + /** + * @return the observable enabled property + */ + public BooleanProperty enabledProperty() { + return enabled; + } + + @Override + public void setEnabled(boolean enabled) { + this.enabled.set(enabled); + } + + /** + * Creates a copy of the data without the listeners + * + * @return a copy without the listeners + */ + public ValueProviderParameterModel copyDataOnly() { + return new ValueProviderParameterModel(this); + } +} diff --git a/src/main/java/com/rapidminer/connection/legacy/ConversionException.java b/src/main/java/com/rapidminer/connection/legacy/ConversionException.java new file mode 100644 index 000000000..9a507b677 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/legacy/ConversionException.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.legacy; + + +/** + * Exception that is thrown by {@link ConversionService#convert(Object)} if the conversion to a + * @link com.rapidminer.connection.ConnectionInformation ConnectionInformation} failed. + * + * @author Jonas Wilms-Pfau + * @since 9.3.0 + */ +public class ConversionException extends Exception { + + /** + * @see Exception#Exception(String) + */ + public ConversionException(String message) { + this(message, null); + } + + /** + * @see Exception#Exception(String, Throwable) + */ + public ConversionException(String message, Exception cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/rapidminer/connection/legacy/ConversionService.java b/src/main/java/com/rapidminer/connection/legacy/ConversionService.java new file mode 100644 index 000000000..2fba4d3f3 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/legacy/ConversionService.java @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.legacy; + +import com.rapidminer.connection.ConnectionInformation; + + +/** + * Implemented by {@link com.rapidminer.connection.ConnectionHandler ConnectionHandler} to offer conversion methods for + * old connection configurations. + * + * @param + * The class of the old connection information + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public interface ConversionService { + + /** + * Verifies that the given {@link Object} is of the right type and can be converted to a {@link ConnectionInformation} + * + * @param oldConnectionObject + * the {@link Object} that should be tested + * @return {@code true} if this {@link Object} can be converted to a {@link ConnectionInformation} + */ + boolean canConvert(Object oldConnectionObject); + + /** + * Converts the given {@link T} to a {@link ConnectionInformation} + * + * @param oldConnectionObject + * the {@link T} that should be converted; must not be {@code null} + * @return the {@link ConnectionInformation result} of the conversion + * @throws ConversionException + * if the conversion failed + * @throws ClassCastException if {@code oldConnectionObject} is not of type {@link T} + */ + ConnectionInformation convert(T oldConnectionObject) throws ConversionException; +} diff --git a/src/main/java/com/rapidminer/connection/util/ConnectionI18N.java b/src/main/java/com/rapidminer/connection/util/ConnectionI18N.java new file mode 100644 index 000000000..d57c59757 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/util/ConnectionI18N.java @@ -0,0 +1,468 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.util; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import javax.swing.Icon; + +import com.rapidminer.connection.valueprovider.ValueProvider; +import com.rapidminer.connection.valueprovider.ValueProviderParameter; +import com.rapidminer.gui.tools.IconSize; +import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.tools.I18N; + + +/** + * Utility class for getting connection related I18N entries. + *

+ * Note: type normally is of the format extension_id:type, which does not work for i18n. + * Therefore, wherever in this i18n the type is used, its colon is replaced by a dot before looking it up, e.g. {@code + * jdbc_connectors.jdbc.host.label} + *

+ * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public final class ConnectionI18N { + + /** + * Delimiter character for keys + */ + public static final String KEY_DELIMITER = "."; + + /** + * Label key suffix + */ + public static final String LABEL_SUFFIX = "label"; + + /** + * Tip key suffix + */ + public static final String TIP_SUFFIX = "tip"; + + /** + * Icon key suffix + */ + public static final String ICON_SUFFIX = "icon"; + + /** + * Prompt key suffix (prompt is the gray text in an empty text field) + */ + public static final String PROMPT_SUFFIX = "prompt"; + + /** + * Connection key prefix + */ + public static final String CONNECTION_PREFIX = "gui.label.connection"; + + /** + * Connection type key prefix + */ + public static final String TYPE_PREFIX = "gui.label.connection.type"; + + /** + * ValueProvider type key prefix + */ + public static final String VALUE_PROVIDER_TYPE_PREFIX = "gui.label.connection.valueprovider.type"; + + /** + * ValueProvider parameter prefix + */ + public static final String VALUE_PROVIDER_PARAMETER_PREFIX = "gui.label.connection.valueprovider.parameter"; + + /** + * Connection group key prefix + */ + public static final String GROUP_PREFIX = "gui.label.connection.group"; + + /** + * Connection parameter key prefix + */ + public static final String PARAMETER_PREFIX = "gui.label.connection.parameter"; + + /** + * Fallback connection icon + */ + public static final String CONNECTION_ICON = I18N.getGUILabel("connection.type.unknown.icon"); + + /** + * stores the icons for all connection types + */ + private static final Map ICON_CONNECTION_TYPE_MAP = Collections.synchronizedMap(new HashMap<>()); + + + /** + * Prevent utility class instantiation. + */ + private ConnectionI18N() { + throw new AssertionError("Utility class"); + } + + /** + * Returns the name of the image for the given connection type. + *

+ * This requires a GUI.properties entry in the form of {@code gui.label.connection.type.{type}.icon} which resolves + * to filename.png + *

+ *

+ * Note: type normally is of the format extension_id:type, which does not work for i18n. + * Therefore, the colon is replaced by a dot before looking it up, e.g. {@code jdbc_connectors.jdbc.host.label} + *

+ * + * @param type + * the {@link com.rapidminer.connection.configuration.ConnectionConfiguration#getType() type} + * @return the image file name, or {@link #CONNECTION_ICON} if not set + */ + public static String getConnectionIconName(String type) { + // Check if a .icon entry exists + String imageName = I18N.getGUIMessageOrNull(String.join(KEY_DELIMITER, TYPE_PREFIX, replaceColon(type), ICON_SUFFIX)); + if (imageName == null) { + imageName = CONNECTION_ICON; + } + return imageName; + } + + /** + * Returns the image for the given connection type. + *

+ * This requires a GUI.properties entry in the form of gui.label.connection.type.{type}.icon which resolves to + * "filename.png", which must be in the icons/16/ resource folder + *

+ *

+ * Note: type normally is of the format extension_id:type, which does not work for i18n. + * Therefore, the colon is replaced by a dot before looking it up, e.g. {@code jdbc_connectors.jdbc.host.label} + *

+ * + * @param type + * the {@link com.rapidminer.connection.configuration.ConnectionConfiguration#getType() type} + * @param iconSize + * the requested icon size. Connections icons can be retrieved in 16px, 24px, and 48px + * @return the defined icon, or the {@link #CONNECTION_ICON} + */ + public static Icon getConnectionIcon(String type, IconSize iconSize) { + String sizePrefix = iconSize.getSize() + "/"; + return ICON_CONNECTION_TYPE_MAP.computeIfAbsent(sizePrefix + type, + k -> SwingTools.createIcon(sizePrefix + getConnectionIconName(type))); + } + + /** + * Returns the icon name for the group of the given type. + *

+ * This requires a GUI.properties entry in the form of gui.label.connection.group.{type}.{group}.icon which resolves + * to "filename.png", which must be in the icons/16/ resource folder + *

+ *

+ * Note: type normally is of the format extension_id:type, which does not work for i18n. + * Therefore, the colon is replaced by a dot before looking it up, e.g. {@code jdbc_connectors.jdbc.host.label} + *

+ * + * @param type + * the {@link com.rapidminer.connection.configuration.ConnectionConfiguration#getType() type} + * @param group + * the {@link com.rapidminer.connection.configuration.ConfigurationParameterGroup#getGroup() group name} + * @return the image file name, or {@code null} if not set + */ + public static String getGroupIconName(String type, String group) { + return getGroupName(type, group, ICON_SUFFIX, null); + } + + /** + * Returns the icon for the group of the given type. + *

+ * This requires a GUI.properties entry in the form of gui.label.connection.group.{type}.{group}.icon which resolves + * to "filename.png", which must be in the icons/16/ resource folder + *

+ *

+ * Note: type normally is of the format extension_id:type, which does not work for i18n. + * Therefore, the colon is replaced by a dot before looking it up, e.g. {@code jdbc_connectors.jdbc.host.label} + *

+ * + * @param type + * the {@link com.rapidminer.connection.configuration.ConnectionConfiguration#getType() type} + * @param group + * the {@link com.rapidminer.connection.configuration.ConfigurationParameterGroup#getGroup() group name} + * @return the image, or {@code null} if not set + */ + public static Icon getGroupIcon(String type, String group) { + String image = getGroupIconName(type, group); + if (image == null) { + return null; + } + return SwingTools.createIcon("icons/16/" + image); + } + + /** + * Returns the I18N name for the given connection type. + * + *

+ * This requires a GUI.properties entry in the form of gui.label.connection.type.{type}.label, otherwise the raw + * type string is returned. + *

+ *

+ * Note: type normally is of the format extension_id:type, which does not work for i18n. + * Therefore, the colon is replaced by a dot before looking it up, e.g. {@code jdbc_connectors.jdbc.host.label} + *

+ * + * @param type + * the {@link com.rapidminer.connection.configuration.ConnectionConfiguration#getType() type} + * @return the i18n type label, or {@code type} + */ + public static String getTypeName(String type) { + if (type == null) { + return null; + } + return Objects.toString(I18N.getGUIMessageOrNull(String.join(KEY_DELIMITER, TYPE_PREFIX, replaceColon(type), LABEL_SUFFIX)), type); + } + + /** + * Returns the I18N name for the given value provider type. + * + *

+ * This requires a GUI.properties entry in the form of gui.label.connection.valueprovider.type.{type}.label, + * otherwise the raw type string is returned. + *

+ * + * @param type + * the {@link ValueProvider#getType()} type} + * @param arguments + * optional arguments for message formatter + * @return the I18N type name, or type + */ + public static String getValueProviderTypeName(String type, Object... arguments) { + return Objects.toString(I18N.getGUIMessageOrNull(String.join(KEY_DELIMITER, VALUE_PROVIDER_TYPE_PREFIX, replaceColon(type), LABEL_SUFFIX), arguments), type); + } + + /** + * Returns the I18N name for the given value provider type. + * + *

+ * This requires a GUI.properties entry in the form of gui.label.connection.valueprovider.parameter.{valueprovidertype}.parameter, otherwise the raw valueprovidertype string is returned. + *

+ * + * @param valueProviderType + * the {@link ValueProvider#getType()} type} + * @param parameter + * the parameter that needs an I18N name + * @return the I18N type name, or type + */ + public static String getValueProviderParameterName(String valueProviderType, String parameter) { + return Objects.toString(I18N.getGUIMessageOrNull(String.join(KEY_DELIMITER, VALUE_PROVIDER_PARAMETER_PREFIX, replaceColon(valueProviderType), KEY_DELIMITER, parameter)), parameter); + } + + /** + * Returns the I18N GUI message for the following key: + *

+ * {@code gui.label.connection.group.{type}.{group}.{suffix}} + *

+ *

+ * Note: type normally is of the format extension_id:type, which does not work for i18n. + * Therefore, the colon is replaced by a dot before looking it up, e.g. {@code jdbc_connectors.jdbc.host.label} + *

+ * + * @param type + * the {@link com.rapidminer.connection.configuration.ConnectionConfiguration#getType() type} + * @param group + * the {@link com.rapidminer.connection.configuration.ConfigurationParameterGroup#getGroup() group name} + * @param suffix + * the suffix to use, e.g. {@value #LABEL_SUFFIX}, {@value #TIP_SUFFIX} or {@value #ICON_SUFFIX} + * @param nullDefault + * the default value to use, might be {@code null} + * @return the i18n entry for the key, or {@code nullDefault} + */ + public static String getGroupName(String type, String group, String suffix, String nullDefault) { + String key = String.join(KEY_DELIMITER, GROUP_PREFIX, replaceColon(type), group, suffix); + return Objects.toString(I18N.getGUIMessageOrNull(key), nullDefault); + } + + /** + * Returns the I18N GUI label for the following key: + *

+ * {@code gui.label.connection.parameter.{type}.{fullyQualifiedKey}.label } + *

+ *

+ * Note: type normally is of the format extension_id:type, which does not work for i18n. + * Therefore, + * the colon is replaced by a dot before looking it up, e.g. {@code jdbc_connectors.jdbc.host.label} + *

+ * + * @param type + * the {@link com.rapidminer.connection.configuration.ConnectionConfiguration#getType() type} + * @param fullyQualifiedKey + * the fully qualified ({@code group.parameter}) parameter key + * @param nullDefault + * the default value to use, might be {@code null} + * @return the internationalized parameterName or {@code parameterName}, if no I18N entry exist + */ + public static String getParameterName(String type, String fullyQualifiedKey, String nullDefault) { + String[] split = fullyQualifiedKey.split("\\.", 2); + if (split.length != 2) { + return nullDefault; + } + String group = split[0]; + String parameter = split[1]; + return getParameterName(type, group, parameter, nullDefault); + } + + /** + * Returns the I18N GUI label for the following key: + *

+ * {@code gui.label.connection.parameter.{type}.{parameterGroup}.{parameterName}.label } + *

+ *

+ * Note: type normally is of the format extension_id:type, which does not work for i18n. + * Therefore, + * the colon is replaced by a dot before looking it up, e.g. {@code jdbc_connectors.jdbc.host.label} + *

+ * + * @param type + * the {@link com.rapidminer.connection.configuration.ConnectionConfiguration#getType() type} + * @param group + * the {@link com.rapidminer.connection.configuration.ConfigurationParameterGroup#getGroup() group name} + * @param parameterName + * the {@link ValueProviderParameter#getName() parameter name} + * @param nullDefault + * the default value to use, might be {@code null} + * @return the i18n entry for the key, or {@code nullDefault} + */ + public static String getParameterName(String type, String group, String parameterName, String nullDefault) { + String key = String.join(KEY_DELIMITER, PARAMETER_PREFIX, replaceColon(type), group, parameterName, LABEL_SUFFIX); + return Objects.toString(I18N.getGUIMessageOrNull(key), nullDefault); + } + + /** + * Returns the I18N GUI tooltip for the following key: + *

+ * {@code gui.label.connection.parameter.{type}.{parameterGroup}.{parameterName}.tip } + *

+ *

+ * Note: type normally is of the format extension_id:type, which does not work for i18n. + * Therefore, the colon is replaced by a dot before looking it up, e.g. {@code jdbc_connectors.jdbc.host.label} + *

+ * + * @param type + * the {@link com.rapidminer.connection.configuration.ConnectionConfiguration#getType() type} + * @param group + * the {@link com.rapidminer.connection.configuration.ConfigurationParameterGroup#getGroup() group name} + * @param parameterName + * the {@link ValueProviderParameter#getName() parameter name} + * @param nullDefault + * the default value to use, might be {@code null} + * @return the i18n entry for the key, or {@code nullDefault} + */ + public static String getParameterTooltip(String type, String group, String parameterName, String nullDefault) { + String key = String.join(KEY_DELIMITER, PARAMETER_PREFIX, replaceColon(type), group, parameterName, TIP_SUFFIX); + return Objects.toString(I18N.getGUIMessageOrNull(key), nullDefault); + } + + /** + * Returns the I18N GUI prompt for the following key: + *

+ * {@code gui.label.connection.parameter.{type}.{parameterGroup}.{parameterName}.prompt } + *

+ *

+ * Note: type normally is of the format extension_id:type, which does not work for i18n. + * Therefore, the colon is replaced by a dot before looking it up, e.g. {@code jdbc_connectors.jdbc.host.label} + *

+ * + * @param type + * the {@link com.rapidminer.connection.configuration.ConnectionConfiguration#getType() type} + * @param group + * the {@link com.rapidminer.connection.configuration.ConfigurationParameterGroup#getGroup() group name} + * @param parameterName + * the {@link ValueProviderParameter#getName() parameter name} + * @param nullDefault + * the default value to use, might be {@code null} + * @return the i18n entry for the key, or {@code nullDefault} + */ + public static String getParameterPrompt(String type, String group, String parameterName, String nullDefault) { + String key = String.join(KEY_DELIMITER, PARAMETER_PREFIX, replaceColon(type), group, parameterName, PROMPT_SUFFIX); + return Objects.toString(I18N.getGUIMessageOrNull(key), nullDefault); + } + + /** + * Returns a connection gui message label + * + * @param key + * The part between {@value CONNECTION_PREFIX} and {@value LABEL_SUFFIX} + * @param arguments + * optional arguments for message formatter + * @return the formatted string for the given key, or the key if no entry exists + */ + public static String getConnectionGUILabel(String key, Object... arguments) { + return Objects.toString(getConnectionGUIMessageOrNull(key + KEY_DELIMITER + LABEL_SUFFIX, arguments), key); + } + + /** + * Returns a connection gui message + * + * @param key + * the part after {@value CONNECTION_PREFIX} + * @param arguments + * i18n arguments + * @return the formatted string for the given key, or the key if no entry exists + */ + public static String getConnectionGUIMessage(String key, Object... arguments) { + return Objects.toString(getConnectionGUIMessageOrNull(key, arguments), key); + } + + /** + * Returns a connection gui message or {@code null} + * + * @param key + * the part after {@value CONNECTION_PREFIX} + * @param arguments + * i18n arguments + * @return the formatted string for the given key, or {@code null} if no entry exists + */ + public static String getConnectionGUIMessageOrNull(String key, Object... arguments) { + return I18N.getGUIMessageOrNull(CONNECTION_PREFIX + KEY_DELIMITER + key, arguments); + } + + /** + * Returns a validation error message from the given i18n key fragment or the key itself if no i18n is found. + * + * @param errorKey + * the i18n error key. Will become part of a composite key before it is being looked up in the GUI.properties file: + * {@code gui.label.connection.validation.i18nkey = {0} lorem ipsum}. + * @param type + * the type of the connection + * @param group + * the group the parameter that failed validation is in + * @param parameterKey + * the key of the parameter that failed validation + * @return the i18n value or the key itself if no i18n is found + */ + public static String getValidationErrorMessage(String errorKey, String type, String group, String parameterKey) { + return getConnectionGUIMessage(errorKey, getParameterName(type, group, parameterKey, parameterKey)); + } + + /** + * Replaces the first colon in the type with a dot. + * + * @param type + * the type, never {@code null} + * @return the type where the first (there should only be one) color is replaced by dot + */ + private static String replaceColon(String type) { + return type.replace(':', '.'); + } +} diff --git a/src/main/java/com/rapidminer/connection/util/ConnectionInformationSelector.java b/src/main/java/com/rapidminer/connection/util/ConnectionInformationSelector.java new file mode 100644 index 000000000..ee233b052 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/util/ConnectionInformationSelector.java @@ -0,0 +1,468 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.util; + +import static com.rapidminer.connection.util.ConnectionI18N.getTypeName; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.rapidminer.connection.ConnectionHandlerRegistry; +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.ConnectionInformationContainerIOObject; +import com.rapidminer.operator.Annotations; +import com.rapidminer.operator.IOObject; +import com.rapidminer.operator.InvalidRepositoryEntryError; +import com.rapidminer.operator.Operator; +import com.rapidminer.operator.PortUserError; +import com.rapidminer.operator.ProcessSetupError; +import com.rapidminer.operator.ProcessSetupError.Severity; +import com.rapidminer.operator.SimpleProcessSetupError; +import com.rapidminer.operator.UserError; +import com.rapidminer.operator.error.ParameterError; +import com.rapidminer.operator.ports.IncompatibleMDClassException; +import com.rapidminer.operator.ports.InputPort; +import com.rapidminer.operator.ports.OutputPort; +import com.rapidminer.operator.ports.Port; +import com.rapidminer.operator.ports.metadata.ConnectionInformationMetaData; +import com.rapidminer.operator.ports.metadata.MetaData; +import com.rapidminer.operator.ports.metadata.SimpleMetaDataError; +import com.rapidminer.operator.ports.metadata.SimplePrecondition; +import com.rapidminer.operator.ports.quickfix.ParameterSettingQuickFix; +import com.rapidminer.parameter.ParameterHandler; +import com.rapidminer.parameter.ParameterType; +import com.rapidminer.parameter.ParameterTypeConnectionLocation; +import com.rapidminer.parameter.conditions.PortConnectedCondition; +import com.rapidminer.repository.ConnectionEntry; +import com.rapidminer.repository.Entry; +import com.rapidminer.repository.MalformedRepositoryLocationException; +import com.rapidminer.repository.RepositoryEntryNotFoundException; +import com.rapidminer.repository.RepositoryEntryWrongTypeException; +import com.rapidminer.repository.RepositoryException; +import com.rapidminer.repository.RepositoryLocation; +import com.rapidminer.tools.I18N; + + +/** + * Helper class that can handle an handler's {@link ConnectionInformation} through a passthrough port/parameter combination. + * Instances can also set up default transformation rules. + * + * @author Jan Czogalla + * @since 9.3 + */ +public class ConnectionInformationSelector { + + public static final String PARAMETER_CONNECTION_ENTRY = "connection_entry"; + + private static final String PARAMETER_DESC_PREFIX = "gui.label.connection.operator_parameter."; + private static final String CONNECTION_MISMATCHED_TYPE = "connection.mismatched_type"; + + private InputPort input; + private OutputPort output; + private ParameterHandler handler; + private String conType; + + /** + * Minimal constructor with related Operator and connection type. Will create an input/output port pair named "connection". + */ + public ConnectionInformationSelector(Operator operator, String conType) { + this(operator.getInputPorts().createPassThroughPort("connection"), operator.getOutputPorts().createPassThroughPort("connection"), operator, conType); + } + + /** + * Sets up an instance with the given handler, port pair (if any) and connection type. + * + * @param input + * the input port; can be {@code null} + * @param output + * the output port; can be {@code null} + * @param handler + * the handler that this instance is coupled with; must not be {@code null} + * @param conType + * the connection type as handled by its corresponding {@link com.rapidminer.connection.ConnectionHandler} + */ + public ConnectionInformationSelector(InputPort input, OutputPort output, ParameterHandler handler, String conType) { + this.input = input; + this.output = output; + this.handler = handler; + this.conType = conType; + } + + /** + * Creates default transformation rules if {@link #input} is not {@code null}. This will add a {@link SimplePrecondition} + * on the input port, and adds a + * {@link com.rapidminer.operator.ports.metadata.MDTransformer#addPassThroughRule passthrough rule}. + */ + public void makeDefaultPortTransformation() { + if (input != null) { + input.addPrecondition(new SimplePrecondition(input, new ConnectionInformationMetaData(), false) { + + @Override + protected boolean isMandatory() { + return portMandatory(); + } + }); + } else { + return; + } + if (output != null && handler instanceof Operator) { + ((Operator) handler).getTransformer().addPassThroughRule(input, output); + } + } + + /** + * @return whether or not the input port is mandatory. Will return {@code false} if the parameter is set + */ + protected boolean portMandatory() { + return !handler.getParameters().getParameterType(PARAMETER_CONNECTION_ENTRY).isHidden() && !handler.getParameters().isSet(PARAMETER_CONNECTION_ENTRY); + } + + /** Get the input port. Might return {@code null}. */ + public InputPort getInput() { + return input; + } + + /** Get the output port. Might return {@code null}. */ + public OutputPort getOutput() { + return output; + } + + /** Get the associated {@link ParameterHandler}. Can be an {@link Operator} */ + public ParameterHandler getHandler() { + return handler; + } + + /** Get the allowed connection type. Can be {@code null}, allowing any connection type */ + public String getConnectionType() { + return conType; + } + + /** + * Checks whether a connection is specified through the port or parameter. Does not test for validity of parameter. + */ + public boolean isConnectionSpecified() { + if (input == null || !input.isConnected()) { + return handler.getParameters().isSet(PARAMETER_CONNECTION_ENTRY); + } + return input.getMetaData() instanceof ConnectionInformationMetaData; + } + + /** + * Get the meta data of the connection if it is properly specified. Otherwise just returns a generic {@link ConnectionInformationMetaData}. + * Will try to either get matching meta data from the input port or from the parameter specified repository location. + */ + public ConnectionInformationMetaData getMetaData() { + try { + return getMetaDataOrThrow(); + } catch (RepositoryException e) { + return new ConnectionInformationMetaData(); + } + } + + /** + * Get the meta data of the connection if it is properly specified. Otherwise returns {@code null} for + * not or wrongly connected inputs, as well as for an unset or unused {@link #PARAMETER_CONNECTION_ENTRY} parameter. + * Will throw repository related exceptions in case of misconfigured parameters. + * + * @return the properly specified meta data or {@code null} + * @throws RepositoryException + * if a repository related problem occurs + */ + public ConnectionInformationMetaData getMetaDataOrThrow() throws RepositoryException { + if (input != null && input.isConnected()) { + MetaData md = input.getMetaData(); + if (md instanceof ConnectionInformationMetaData) { + return (ConnectionInformationMetaData) md; + } + // wrong kind of MD or no MD => taken care of by precondition + return null; + } + if (handler.getParameters().getParameterType(PARAMETER_CONNECTION_ENTRY).isHidden()) { + return null; + } + if (!handler.getParameters().isSet(PARAMETER_CONNECTION_ENTRY)) { + // parameter not set => but is mandatory + return null; + } + RepositoryLocation location; + try { + location = getRepoLocationFromParameter(); + } catch (UserError e) { + throw new RepositoryException(e.getMessage()); + } + Entry entry = location.locateEntry(); + if (entry == null) { + throw new RepositoryEntryNotFoundException(location); + } + if (!(entry instanceof ConnectionEntry)) { + throw new RepositoryEntryWrongTypeException(location, ConnectionEntry.TYPE_NAME, entry.getType()); + } + MetaData md = ((ConnectionEntry) entry).retrieveMetaData(); + if (!(md instanceof ConnectionInformationMetaData)) { + throw new RepositoryEntryWrongTypeException(location, ConnectionEntry.TYPE_NAME, + md == null ? null : md.getObjectClass().getSimpleName()); + } + ConnectionInformationMetaData metaData = (ConnectionInformationMetaData) md; + Annotations annotations = metaData.getAnnotations(); + if (annotations == null) { + annotations = new Annotations(); + metaData.setAnnotations(annotations); + } + annotations.setAnnotation(Annotations.KEY_SOURCE, entry.getLocation().toString()); + return metaData; + } + + /** + * Returns the selected {@link ConnectionInformation} if any. Will prefer the input port over the parameter. + * Will throw a {@link UserError} if any error occurs, i.e. if the data is not present or does not match. + * + * @return a connection information, never {@code null} + * @throws UserError + * if an error occurs + */ + public ConnectionInformation getConnection() throws UserError { + ConnectionInformationContainerIOObject container; + boolean connectionFromPort = input != null && input.isConnected(); + if (connectionFromPort) { + container = input.getDataOrNull(ConnectionInformationContainerIOObject.class); + if (container == null) { + // no data (yet)? => infer repo location from meta data + RepositoryLocation location = getRepoLocationFromInputMD(input); + if (location != null) { + container = extractConnectionFromLocation(location); + } + } + } else { + if (!handler.getParameters().isSet(PARAMETER_CONNECTION_ENTRY)) { + throw new UserError(null, "connection.no_connection"); + } + RepositoryLocation location = getRepoLocationFromParameter(); + container = extractConnectionFromLocation(location); + } + if (container == null) { + throw new UserError(null, "connection.no_container"); + } + // don't use the original + ConnectionInformation ci = container.getConnectionInformation().copy(); + + if (conType != null) { + String actualType = ci.getConfiguration().getType(); + if (!conType.equals(actualType)) { + if (connectionFromPort) { + throw new PortUserError(input, CONNECTION_MISMATCHED_TYPE, + getTypeName(conType), getTypeName(actualType)); + } else { + throw new ParameterError(null, CONNECTION_MISMATCHED_TYPE, + PARAMETER_CONNECTION_ENTRY, getTypeName(conType), getTypeName(actualType)); + } + } + } + return ci; + } + + /** + * Checks that this {@link ConnectionInformationSelector} has a correctly defined connection at hand, + * if that is indicated by a connected port or the parameter {@link #PARAMETER_CONNECTION_ENTRY}. + * Will return a {@link ProcessSetupError} if one of the following holds: + *
    + *
  • A repository error occurred when trying to retrieve the information
  • + *
  • No handler was registered for the {@link #conType} specified here, if that type is not {@code null}
  • + *
  • The provided connection type and the specified connection type don't match
  • + *
+ * The last case can be separated into two subcases; if the connection is provided by the {@link InputPort}, + * then the returned error is a {@link SimpleMetaDataError}. If the connection is provided by the parameter, + * the error is an {@link InvalidRepositoryEntryError}. + * + * @param operator + * the operator to check on + * @return {@code null} if no errors occurred, otherwise an error as specified above + * @see #getMetaDataOrThrow() + */ + public ProcessSetupError checkConnectionTypeMatch(Operator operator) { + ConnectionInformationMetaData metaData; + List parameterSetting = Collections.singletonList(new ParameterSettingQuickFix(operator, PARAMETER_CONNECTION_ENTRY)); + try { + metaData = getMetaDataOrThrow(); + if (metaData == null) { + return null; + } + } catch (RepositoryException e) { + String errorKey = e.getMessage(); + boolean isAbsolute = errorKey != null; + if (!isAbsolute) { + errorKey = "connection.repository_error"; + } + return new SimpleProcessSetupError(Severity.ERROR, operator.getPortOwner(), + parameterSetting, isAbsolute, errorKey); + } + String wantedType = getConnectionType(); + if (wantedType == null) { + return null; + } + if (!ConnectionHandlerRegistry.getInstance().isTypeKnown(wantedType)) { + return new SimpleProcessSetupError(Severity.ERROR, operator.getPortOwner(), "connection.no_handler_registered"); + } + String foundType = metaData.getConnectionType(); + if (foundType == null || wantedType.equals(foundType)) { + return null; + } + String errorKey = CONNECTION_MISMATCHED_TYPE; + String wantedTypeName = getTypeName(wantedType); + String foundTypeName = getTypeName(foundType); + Port conInput = getInput(); + boolean connectionFromPort = conInput != null && conInput.isConnected(); + if (connectionFromPort) { + return new SimpleMetaDataError(Severity.ERROR, conInput, Collections.emptyList(), errorKey, wantedTypeName, foundTypeName); + } else { + return new SimpleProcessSetupError(Severity.ERROR, operator.getPortOwner(), + parameterSetting, errorKey, wantedTypeName, foundTypeName); + } + } + + /** Passes matching data through if both input and output ports exist. */ + public void passDataThrough() { + if (input != null && output != null) { + IOObject data = null; + try { + data = input.getDataOrNull(ConnectionInformationContainerIOObject.class); + } catch (UserError userError) { + // ignore; do nothing + } + output.deliver(data); + } + } + + /** Passes matching data through as a copy if both input and output ports exist. */ + public void passCloneThrough() { + if (input != null && output != null) { + IOObject data = null; + try { + data = input.getDataOrNull(ConnectionInformationContainerIOObject.class); + } catch (UserError userError) { + // ignore; do nothing + } + output.deliver(data == null ? null : data.copy()); + } + } + + /** + * Extracts a repository location from the input ports {@link MetaData} if possible. + * For this to work, the input port needs to be connected and have correct {@link MetaData} + * of type {@link ConnectionInformationMetaData} that are annotated with {@value Annotations#KEY_SOURCE}. + * This annotation needs to be a valid {@link RepositoryLocation} which will then be returned. + * Otherwise this method returns {@code null}. + * + * @param input + * the input port to check for meta data; must not be {@code null} + * @return a valid repository location or {@code null} + * @see com.rapidminer.operator.io.RepositorySource#getGeneratedMetaData() RepositorySource.getGeneratedMetaData() + */ + private RepositoryLocation getRepoLocationFromInputMD(InputPort input) { + MetaData md; + try { + md = input.getMetaData(ConnectionInformationMetaData.class); + if (md != null && md.getAnnotations() != null) { + String source = md.getAnnotations().getAnnotation(Annotations.KEY_SOURCE); + if (source != null) { + return new RepositoryLocation(source); + } + } + } catch (IncompatibleMDClassException | MalformedRepositoryLocationException e) { + // ignore + } + return null; + } + + /** + * Extracts a {@link ConnectionInformationContainerIOObject} from the given {@link RepositoryLocation} if possible. + * If the associated {@link Entry} is not a {@link ConnectionEntry}, no (valid) data is stored there or a {@link RepositoryException} + * occurs, this method will tjrow a corresponding {@link UserError}. + * + * @param location + * the repository location to check/extract from; must not be {@code null} + * @return a valid connection container, never {@code null} + * @throws UserError + * if an error occurs + */ + private ConnectionInformationContainerIOObject extractConnectionFromLocation(RepositoryLocation location) throws UserError { + ConnectionInformationContainerIOObject container; + try { + Entry entry = location.locateEntry(); + if (!(entry instanceof ConnectionEntry)) { + throw new UserError(null, "connection.wrong_entry_type"); + } + IOObject data = ((ConnectionEntry) entry).retrieveData(null); + if (!(data instanceof ConnectionInformationContainerIOObject)) { + throw new UserError(null, "connection.wrong_entry_data"); + } + container = (ConnectionInformationContainerIOObject) data; + } catch (RepositoryException e) { + throw new UserError(null, e, "connection.repository_error", location.getName()); + } + return container; + } + + /** Resolves the repository location. Will make a distinction between an operater and a simple parameter handler */ + private RepositoryLocation getRepoLocationFromParameter() throws UserError { + return RepositoryLocation.getRepositoryLocation(handler.getParameterAsString(PARAMETER_CONNECTION_ENTRY), + handler instanceof Operator ? (Operator) handler : null); + } + + /** + * Creates and returns the list of parameters associated with the given selector. + * By default this contains only one parameter of type {@link ParameterTypeConnectionLocation} that will be hidden while + * the input port is connected. + */ + public static List createParameterTypes(ConnectionInformationSelector cis) { + ArrayList types = new ArrayList<>(); + ParameterType type = new ParameterTypeConnectionLocation(PARAMETER_CONNECTION_ENTRY, + createConnectionEntryDescription(PARAMETER_CONNECTION_ENTRY, cis.conType, + "Select a connection from a repository"), cis.conType); + if (cis.input != null) { + type.registerDependencyCondition(new PortConnectedCondition(cis.handler, () -> cis.input, true, false)); + } + types.add(type); + return types; + } + + /** + * Create an i18n compliant description if possible. Looks up the i18n key + * {@value #PARAMETER_DESC_PREFIX}{@code {key}.[any|type].desc}, and uses the i18n name of the connection type + * if possible. + * + * @param key + * the parameter key + * @param conType + * the connection type + * @param defaultDescription + * the description to be used if no i18n is available + * @return the i18n or default description + */ + private static String createConnectionEntryDescription(String key, String conType, String defaultDescription) { + String conTypeName = ""; + String typeKey = ".any"; + if (conType != null && ConnectionHandlerRegistry.getInstance().isTypeKnown(conType)) { + conTypeName = getTypeName(conType); + typeKey = ".type"; + } + String description = I18N.getGUIMessageOrNull(PARAMETER_DESC_PREFIX + key + typeKey + ".desc", conTypeName); + return description != null ? description : defaultDescription; + } +} diff --git a/src/main/java/com/rapidminer/connection/util/ConnectionSelectionProvider.java b/src/main/java/com/rapidminer/connection/util/ConnectionSelectionProvider.java new file mode 100644 index 000000000..899d1c1b7 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/util/ConnectionSelectionProvider.java @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.util; + +/** + * Helper interface for operators that use a {@link ConnectionInformationSelector} in the new connection management + * + * @author Jan Czogalla + * @since 9.3 + */ +public interface ConnectionSelectionProvider { + + /** + * Returns the {@link ConnectionInformationSelector} associated with this instance. Might be {@code null} + */ + ConnectionInformationSelector getConnectionSelector(); + + /** + * Sets the {@link ConnectionInformationSelector} for this instance. + */ + void setConnectionSelector(ConnectionInformationSelector selector); +} diff --git a/src/main/java/com/rapidminer/connection/util/GenericHandler.java b/src/main/java/com/rapidminer/connection/util/GenericHandler.java new file mode 100644 index 000000000..46a49a6a4 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/util/GenericHandler.java @@ -0,0 +1,71 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.util; + + +/** + * A generic handler interface. Provides initialization, type information and testing functionality. + * + * @param + * the class whose objects are handled + * @author Jan Czogalla + * @see GenericHandlerRegistry + * @since 9.3 + */ +public interface GenericHandler { + + /** + * Initialize the handler. This should be called once, before registering the handler. + */ + void initialize(); + + /** + * Whether the handler was already initialized + */ + boolean isInitialized(); + + /** + * The type of this handler. This must be unique between all handlers. + * If the handler was registered from an {@link com.rapidminer.tools.plugin.Plugin Extension}, + * the type should be in the form of {@code namespace:type}. + */ + String getType(); + + /** + * Validates the given handled object. This can be used to check if all parameters have sensible values. + * This should only take a very small amount of time as opposed to {@link #test(TestExecutionContext)} which might run a longer operation. + * Should return a {@link com.rapidminer.connection.util.TestResult.ResultType#NONE ResultType.NONE} result + * if the object is {@code null} + * + * @param object + * the object to be validated + * @see ValidationResult + */ + ValidationResult validate(T object); + + /** + * Test if the given handled object is configured correctly; should return a + * {@link com.rapidminer.connection.util.TestResult.ResultType#NONE ResultType.NONE} result + * if the object is {@code null} + * + * @param testContext + * the execution context containing the test subject to be tested + */ + TestResult test(TestExecutionContext testContext); +} diff --git a/src/main/java/com/rapidminer/connection/util/GenericHandlerRegistry.java b/src/main/java/com/rapidminer/connection/util/GenericHandlerRegistry.java new file mode 100644 index 000000000..fa16c112e --- /dev/null +++ b/src/main/java/com/rapidminer/connection/util/GenericHandlerRegistry.java @@ -0,0 +1,191 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.util; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import javax.swing.event.EventListenerList; + +import com.rapidminer.connection.util.RegistrationEvent.RegistrationEventType; +import com.rapidminer.tools.I18N; +import com.rapidminer.tools.ValidationUtil; + + +/** + * Generic handler registry for {@link GenericHandler} subinterfaces. Handlers can be registered and unregistered, + * searched by type and (un)registrations can be observed. + * + * @param + * the handler subclass/-interface + * @author Jan Czogalla + * @since 9.3 + */ +public abstract class GenericHandlerRegistry { + + /** + * {@link IllegalArgumentException} to indicate a missing handler for a given type. + * + * @author Jan Czogalla + * @since 9.3 + */ + public static class MissingHandlerException extends IllegalArgumentException { + + private static final String ERROR_PREFIX = "generic_registry.missing_handler."; + private static final String ERROR_CORE = "core"; + private static final String ERROR_EXTENSION = "extension"; + private static final String TYPE_PREFIX = "generic_registry.handler.type."; + + /** @see #createMessage(String, String) */ + private MissingHandlerException(String type, String handlerType) { + super(createMessage(type, handlerType)); + } + + /** + * Creates a new i18n message for a missing handler. + * Resolves error message for + *

+ * {@value #ERROR_PREFIX}{@code [}{@value #ERROR_CORE}{@code |}{@value #ERROR_EXTENSION}{@code ]} + *

+ * depending on whether the given type indicates its origin and uses the registryType to resolve its i18n name by looking up + *

+ * {@value #TYPE_PREFIX}{@code registryType} + * + * @param type + * the type that is missing a handler + * @param registryType + * the type of the registry + * @return the {@link I18N} message for the missing handler + * @see GenericHandler#getType() + * @see GenericHandlerRegistry#getRegistryType() + */ + private static String createMessage(String type, String registryType) { + int namespacePos = type.indexOf(':'); + String namespace = null; + if (namespacePos >= 0) { + namespace = type.substring(0, namespacePos); + } + String errorKey = ERROR_PREFIX; + if (namespace == null) { + errorKey += ERROR_CORE; + } else { + errorKey += ERROR_EXTENSION; + } + String handlerTypeName = Objects.toString(I18N.getErrorMessageOrNull(TYPE_PREFIX + registryType), ""); + return I18N.getErrorMessage(errorKey, handlerTypeName, type, namespace); + } + } + + private Map registeredHandlers = new HashMap<>(); + private EventListenerList eventListeners = new EventListenerList(); + + /** Abstract singleton class */ + protected GenericHandlerRegistry() { + } + + /** + * Register new {@link H handler}. Must not be {@code null}. If a handler with the same type was already + * registered, does nothing. If the handler is successfully registered, + * triggers a {@link RegistrationEventType#REGISTERED REGISTERED} event. + */ + public void registerHandler(H handler) { + ValidationUtil.requireNonNull(handler, "handler"); + if (registeredHandlers.putIfAbsent(ValidationUtil.requireNonEmptyString(handler.getType(), "handler type"), handler) == null) { + fireRegistryEvent(handler, RegistrationEventType.REGISTERED); + } + } + + /** + * Unregister a {@link H handler}. Must not be {@code null}. Only can be removed if exactly this handler + * was registered before using {@link #registerHandler(H)}. If the handler is successfully unregistered, + * triggers a {@link RegistrationEventType#UNREGISTERED UNREGISTERED} event. + */ + public void unregisterHandler(H handler) { + ValidationUtil.requireNonNull(handler, "handler"); + if (registeredHandlers.remove(ValidationUtil.requireNonEmptyString(handler.getType(), "handler type"), handler)) { + fireRegistryEvent(handler, RegistrationEventType.UNREGISTERED); + } + } + + /** Add an event listener for {@link RegistrationEvent RegistrationEvents} */ + public > void addEventListener(L listener) { + ValidationUtil.requireNonNull(listener, "listener"); + eventListeners.add(getListenerClass(listener), listener); + } + + /** Remove the specified event listener for {@link RegistrationEvent RegistrationEvents} */ + public > void removeEventListener(L listener) { + ValidationUtil.requireNonNull(listener, "listener"); + eventListeners.remove(getListenerClass(listener), listener); + } + + /** + * Get the handler for the specified type if one is registered. Otherwise throws a {@link MissingHandlerException} + * with more details. + * + * @param type + * the type to get the handler for + * @return the handler for the given type if it exists, never {@code null} + * @throws MissingHandlerException + * if the type is not known + * @see #isTypeKnown(String) + */ + public H getHandler(String type) throws MissingHandlerException { + ValidationUtil.requireNonEmptyString(type, "type"); + H handler = registeredHandlers.get(type); + if (handler == null) { + throw new MissingHandlerException(type, getRegistryType()); + } + return handler; + } + + /** + * Check whether a handler is registered for the given type. If {@code true}, it is safe to call {@link #getHandler(String)} + * + * @param type + * the type to check + * @return whether the type is known, i.e. has a corresponding handler + */ + public boolean isTypeKnown(String type) { + return registeredHandlers.containsKey(type); + } + + /** Returns a list of all registered handlers */ + public List getAllTypes() { + return new ArrayList<>(registeredHandlers.keySet()); + } + + /** Returns either an implementation specific marker interface or the listeners class */ + protected abstract , L extends G> Class getListenerClass(L listener); + + /** + * Returns this registry's type. This is used for some i18n handling and should have an error entry + * {@value MissingHandlerException#TYPE_PREFIX}{@code type} + */ + protected abstract String getRegistryType(); + + /** Fire a {@link RegistrationEvent} of the given {@link RegistrationEventType} regarding the specified handler. */ + private void fireRegistryEvent(H handler, RegistrationEventType eventType) { + for (GenericRegistrationEventListener listener : eventListeners.getListeners(getListenerClass(null))) { + listener.registrationChanged(new RegistrationEvent(this, eventType), handler); + } + } +} diff --git a/src/main/java/com/rapidminer/connection/util/GenericRegistrationEventListener.java b/src/main/java/com/rapidminer/connection/util/GenericRegistrationEventListener.java new file mode 100644 index 000000000..fb22fe688 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/util/GenericRegistrationEventListener.java @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.util; + +import java.util.EventListener; + +/** + * A generic {@link EventListener} for registrations, uses {@link RegistrationEvent RegistrationEvents} + * + * @author Jan Czogalla + * @see GenericHandlerRegistry + * @since 9.3 + */ +public interface GenericRegistrationEventListener extends EventListener { + + /** + * Process the changed object + * + * @param event + * the event; should not be {@code null} + * @param changedObject + * the object that changed; should not be {@code null} + */ + void registrationChanged(RegistrationEvent event, H changedObject); +} \ No newline at end of file diff --git a/src/main/java/com/rapidminer/connection/util/ProgressAdapter.java b/src/main/java/com/rapidminer/connection/util/ProgressAdapter.java new file mode 100644 index 000000000..788200e89 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/util/ProgressAdapter.java @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.util; + +import com.rapidminer.tools.ProgressListener; + + +/** + * An abstract adapter class for receiving {@link ProgressListener progress listener} events. The methods in the class + * are empty. This class exists as convenience for creating listener objects. + *

+ * Extend this class to create a {@link ProgressListener} listener and override the methods for the events of interest. + * (If you implement the {@link ProgressListener} interface, you have to define all of the methods in it. This abstract + * class defines empty methods for them all, so you can only have to define methods for events you care about.) + *

+ * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +abstract class ProgressAdapter implements ProgressListener { + + @Override + public void setTotal(int total) { + // do nothing + } + + @Override + public void setCompleted(int completed) { + // do nothing + } + + @Override + public void complete() { + // do nothing + } + + @Override + public void setMessage(String message) { + // do nothing + } +} diff --git a/src/main/java/com/rapidminer/connection/util/RegistrationEvent.java b/src/main/java/com/rapidminer/connection/util/RegistrationEvent.java new file mode 100644 index 000000000..e1a0d6b77 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/util/RegistrationEvent.java @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.util; + +import java.util.EventObject; + +/** + * A simple {@link EventObject} with a source and {@link RegistrationEventType}. + * + * @author Jan Czogalla + * @since 9.3 + */ +public class RegistrationEvent extends EventObject { + + public enum RegistrationEventType { + REGISTERED, UNREGISTERED + } + + private final RegistrationEventType type; + + /** + * Constructs a registration event. + * + * @param source + * The object on which the Event initially occurred. + * @throws IllegalArgumentException + * if source is null. + */ + public RegistrationEvent(Object source, RegistrationEventType type) { + super(source); + this.type = type; + } + + /** Gets the type of event, one of {@link RegistrationEventType} */ + public RegistrationEventType getType() { + return type; + } +} diff --git a/src/main/java/com/rapidminer/connection/util/TestExecutionContext.java b/src/main/java/com/rapidminer/connection/util/TestExecutionContext.java new file mode 100644 index 000000000..f7609bd8d --- /dev/null +++ b/src/main/java/com/rapidminer/connection/util/TestExecutionContext.java @@ -0,0 +1,66 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.util; + +import com.rapidminer.gui.tools.ProgressThreadStoppedException; +import com.rapidminer.tools.ProgressListener; + + +/** + * This context is passed when an object should be tested. This is a generic interface not tied to a particular test + * case. + * + *
    + *
  • Call {@link #checkCancelled()} between expensive operations
  • + *
  • Update the {@link #getProgressListener() test progress} if possible
  • + *
+ * + * @param + * the type of the test subject + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public interface TestExecutionContext { + + /** + * The test subject that should be tested + * + * @return the test subject + * @throws ProgressThreadStoppedException in case the test was cancelled + */ + T getSubject(); + + /** + * Checks if the user has requested to cancel the test, + * if yes throws a {@link ProgressThreadStoppedException} which doesn't have to be handled. + * + * @throws ProgressThreadStoppedException in case the test was cancelled + */ + void checkCancelled() throws ProgressThreadStoppedException; + + /** + * Returns a Progress Listener that can be updated with the current test progress + * + *

Every method of the returned {@link ProgressListener} might throw a {@link ProgressThreadStoppedException}.

+ * + * @return the updatable progress listener, never {@code null} + * @throws ProgressThreadStoppedException in case the test was cancelled + */ + ProgressListener getProgressListener(); +} diff --git a/src/main/java/com/rapidminer/connection/util/TestExecutionContextImpl.java b/src/main/java/com/rapidminer/connection/util/TestExecutionContextImpl.java new file mode 100644 index 000000000..fa4f748a4 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/util/TestExecutionContextImpl.java @@ -0,0 +1,94 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.util; + +import java.util.function.BooleanSupplier; + +import com.rapidminer.gui.tools.ProgressThread; +import com.rapidminer.gui.tools.ProgressThreadStoppedException; +import com.rapidminer.tools.ProgressListener; + + +/** + * Wrapper for test subjects that should be tested in a {@link TestExecutionContext}. + * + * @param + * the type of the test subject + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public class TestExecutionContextImpl implements TestExecutionContext { + + /** The test subject */ + private final T subject; + /** The test progress */ + private final ProgressListener progressListener; + /** Returns {@code true} if test should be stopped */ + private final BooleanSupplier isCancelled; + + /** + * Creates a new test execution context without a progress thread. + * + * @param subject + * the test subject + */ + public TestExecutionContextImpl(T subject) { + this(subject, null); + } + + /** + * Creates a new test execution context with the given progress thread. + * + * @param subject + * the test subject + * @param progressThread + * the progress thread in which the test is executed + */ + public TestExecutionContextImpl(T subject, ProgressThread progressThread) { + this.subject = subject; + if (progressThread != null) { + this.progressListener = progressThread.getProgressListener(); + this.isCancelled = progressThread::isCancelled; + } else { + this.progressListener = new ProgressAdapter() { + // does nothing + }; + this.isCancelled = () -> false; + } + } + + @Override + public T getSubject() { + checkCancelled(); + return subject; + } + + @Override + public void checkCancelled() { + if (isCancelled.getAsBoolean()) { + throw new ProgressThreadStoppedException(); + } + } + + @Override + public ProgressListener getProgressListener() { + checkCancelled(); + return progressListener; + } +} diff --git a/src/main/java/com/rapidminer/connection/util/TestResult.java b/src/main/java/com/rapidminer/connection/util/TestResult.java new file mode 100644 index 000000000..2da37fddf --- /dev/null +++ b/src/main/java/com/rapidminer/connection/util/TestResult.java @@ -0,0 +1,170 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.util; + +import java.util.Collections; +import java.util.Map; + + +/** + * A POJO to represent a generic test result. Consists of a {@link ResultType}, a messageKey which will be used to get + * the i18n result message, and an optional map of parameter i18n error message keys if the test result was caused by specific parameters. + * + * @author Jan Czogalla, Marco Boeck + * @since 9.3 + */ +public class TestResult { + + /** Enum of results for {@link TestResult} */ + public enum ResultType { + /** Indicates that there is no testing procedure */ + NOT_SUPPORTED, + /** Indicates a successful test */ + SUCCESS, + /** Indicates a failed test */ + FAILURE, + /** Indicates that no test could be performed */ + NONE + } + + private static final TestResult NULL_TEST = new TestResult(ResultType.NONE, "test_object_null", null); + /** use this i18n constant when the test succeeded */ + public static final String I18N_KEY_SUCCESS = "test.success"; + /** use this i18n constant for generic test fails */ + public static final String I18N_KEY_FAILED = "test.connection_failed"; + /** use this i18n constant when the test failed because of injection problems */ + public static final String I18N_KEY_INJECTION_FAILURE = "test.injection_failed"; + /** use this i18n constant when the test functionality is not implemented */ + public static final String I18N_KEY_NOT_IMPLEMENTED = "test.not_implemented"; + + private ResultType type; + private String messageKey; + private Map parameterErrorMessages; + private Object[] arguments; + + + /** + * Public access to this object should be done via the static factory methods. + * + * @param type + * the type of the result + * @param messageKey + * the i18n key that will be used to find a human-readable message + * @param parameterErrorMessages + * Optional. Can contain i18n keys for individual parameter error messages if the test result is based on e.g. + * missing/wrong values for one or more individual parameters. Format is: {@code group.parameter - i18nKey} + * @param arguments + * optional arguments for the i18n message + */ + public TestResult(ResultType type, String messageKey, Map parameterErrorMessages, Object... arguments) { + if (type == null) { + throw new IllegalArgumentException("type must not be null!"); + } + if (messageKey == null || messageKey.trim().isEmpty()) { + throw new IllegalArgumentException("messageKey must not be null or empty!"); + } + if (parameterErrorMessages == null) { + parameterErrorMessages = Collections.emptyMap(); + } + + this.type = type; + this.messageKey = messageKey; + this.parameterErrorMessages = parameterErrorMessages; + this.arguments = arguments; + } + + /** Gets the result type */ + public ResultType getType() { + return type; + } + + /** + * Gets the result message i18n key. + * @return the key or message, never {@code null} + */ + public String getMessageKey() { + return messageKey; + } + + /** + * Gets the result message arguments, if any. + * + * @return the arguments or an empty array if there are none + */ + public Object[] getArguments() { + return arguments; + } + + /** + * Get the map of message i18n keys associated with their respective parameters. Each entry has the following + * format: {@code group.key - i18nkey}. The i18n key provided here will become part of a composite key before it is + * being looked up in the i18n file. + * + * @return the map which may be empty but never {@code null} + */ + public Map getParameterErrorMessages() { + return parameterErrorMessages; + } + + /** + * Create a successful test result. + * + * @param messageKey + * the message i18n key, never {@code null} + * @return the test result instance + */ + public static TestResult success(String messageKey) { + return new TestResult(ResultType.SUCCESS, messageKey, null); + } + + /** + * Create a failure test result. + * + * @param messageKey + * the message i18n key, never {@code null} + * @param arguments + * optional additional arguments for the i18n + * @return the test result instance + */ + public static TestResult failure(String messageKey, Object... arguments) { + return failure(messageKey, null, arguments); + } + + /** + * Create a failure test result. + * + * @param messageKey + * the message i18n key, never {@code null} + * @param parameterErrors + * Optional, can be used to indicate one or more errors directly mapped to parameters (e.g. missing value, wrong + * type, etc). Each entry in the map must have the following format: {@code group.key - i18nkey}. Can be {@code + * null} + * @param arguments + * optional additional arguments for the i18n + * @return the test result instance + */ + public static TestResult failure(String messageKey, Map parameterErrors, Object... arguments) { + return new TestResult(ResultType.FAILURE, messageKey, parameterErrors, arguments); + } + + /** Get a test result that indicates the object to test was {@code null}. */ + public static TestResult nullable() { + return NULL_TEST; + } +} diff --git a/src/main/java/com/rapidminer/connection/util/ValidationResult.java b/src/main/java/com/rapidminer/connection/util/ValidationResult.java new file mode 100644 index 000000000..224a21de1 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/util/ValidationResult.java @@ -0,0 +1,117 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.util; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +/** + * A POJO to represent a generic validation result. Consists of a {@link ResultType} and a message connected to the result type. + * In case of a failure, a map of parameter names/error_i18n keys can be retrieved using {@link #getParameterErrorMessages()}. + * + * @author Jan Czogalla + * @since 9.3 + */ +public class ValidationResult extends TestResult { + + private static final ValidationResult NULL_VALIDATION = new ValidationResult(TestResult.nullable()); + /** use this i18n constant when the validation succeeded */ + public static final String I18N_KEY_SUCCESS = "validation.success"; + /** use this i18n constant when the validation failed */ + public static final String I18N_KEY_FAILURE = "validation.failed"; + /** use this i18n constant as an error indicator when the user did not put in a value */ + public static final String I18N_KEY_VALUE_MISSING = "validation.value_missing"; + /** use this i18n constant as an error indicator when a value collapses to {@code null} due to placeholders */ + public static final String I18N_KEY_VALUE_MISSING_PLACEHOLDER = "validation.value_missing_placeholder"; + /** use this i18n constant as an error indicator when a value provider is not working properly */ + public static final String I18N_KEY_VALUE_NOT_INJECTABLE = "validation.value_not_injectable"; + /** Ordering for ResultTypes when merging */ + public static final List RESULT_TYPES_ORDER = Collections.unmodifiableList(Arrays.asList(ResultType.NONE, ResultType.SUCCESS, ResultType.NOT_SUPPORTED, ResultType.FAILURE)); + + + private ValidationResult(TestResult result) { + super(result.getType(), result.getMessageKey(), result.getParameterErrorMessages(), result.getArguments()); + } + + /** + * Create a successful validation result. + * + * @param messageKey + * the message i18n key, never null + */ + public static ValidationResult success(String messageKey) { + return new ValidationResult(TestResult.success(messageKey)); + } + + /** + * Create a failure validation result. + * + * @param messageKey + * the message i18n key, never {@code null} + * @param parameterErrors + * Optional, can be used to indicate one or more errors directly mapped to parameters (e.g. missing value, wrong + * type, etc). Each entry in the map must have the following format: {@code group.key - i18nkey}. Can be {@code + * null} + * @param arguments + * optional additional arguments for the i18n + * @return the test result instance + */ + public static ValidationResult failure(String messageKey, Map parameterErrors, Object... arguments) { + return new ValidationResult(TestResult.failure(messageKey, parameterErrors, arguments)); + } + + /** + * Get a validation result that indicates the object to validate was {@code null}. + */ + public static ValidationResult nullable() { + return NULL_VALIDATION; + } + + /** + * Merge all the given {@link ValidationResult ValidationResults} pessimistically, keep all the parameter errors but + * only the worst result message based on the order + *

+ * ResultType.NONE < ResultType.SUCCESS < ResultType.NOT_SUPPORTED < ResultType.FAILURE + *

+ * Parameter errors of higher ranked {@link ValidationResult ValidationResults} will overwrite existing ones + * + * @param validationResults + * all the validationResults to be merged. + * @return the resulting {@link ValidationResult} with all the parameter errors + */ + public static ValidationResult merge(ValidationResult... validationResults) { + if (validationResults == null || validationResults.length == 0) { + return nullable(); + } + + Map mergedErrors = new HashMap<>(); + + Arrays.sort(validationResults, Comparator.comparingInt(o -> RESULT_TYPES_ORDER.indexOf(o.getType()))); + ValidationResult worstVR = validationResults[validationResults.length - 1]; + for (ValidationResult vr : validationResults) { + mergedErrors.putAll(vr.getParameterErrorMessages()); + } + return new ValidationResult(new TestResult(worstVR.getType(), worstVR.getMessageKey(), mergedErrors, worstVR.getArguments())); + } +} diff --git a/src/main/java/com/rapidminer/connection/valueprovider/ValueProvider.java b/src/main/java/com/rapidminer/connection/valueprovider/ValueProvider.java new file mode 100644 index 000000000..e4e1a7927 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/valueprovider/ValueProvider.java @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.valueprovider; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + + +/** + * Interface for a value provider. Consists of a unique name/type combination + * (per {@link com.rapidminer.connection.configuration.ConnectionConfiguration ConnectionConfiguration}) + * and a (possibly empty) list of {@link ValueProviderParameter ValueProviderParameters}. These parameters must be unique + * as defined by {@link ValueProviderParameter#UNIQUE_NAME_COMPARATOR}. + *

+ * Value providers can be used to inject values into parameters of a {@link com.rapidminer.connection.configuration.ConnectionConfiguration ConnectionConfiguration}. + * The actual functionality is provided in an implementation of {@link com.rapidminer.connection.valueprovider.handler.ValueProviderHandler ValueProviderHandler} + * that was registered with the {@link com.rapidminer.connection.valueprovider.handler.ValueProviderHandlerRegistry ValueProviderHandlerRegistry}. + * + * @author Jan Czogalla + * @since 9.3 + */ +@JsonDeserialize(as = ValueProviderImpl.class) +public interface ValueProvider { + + /** Get the name */ + String getName(); + + /** Get the type */ + String getType(); + + /** Get a copied list of the parameters */ + List getParameters(); + + /** Get a copy of the map of fully qualified parameter keys to parameters */ + Map getParameterMap(); +} diff --git a/src/main/java/com/rapidminer/connection/valueprovider/ValueProviderImpl.java b/src/main/java/com/rapidminer/connection/valueprovider/ValueProviderImpl.java new file mode 100644 index 000000000..e023c0d0a --- /dev/null +++ b/src/main/java/com/rapidminer/connection/valueprovider/ValueProviderImpl.java @@ -0,0 +1,145 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.valueprovider; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import org.apache.commons.collections4.CollectionUtils; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.rapidminer.tools.ValidationUtil; + + +/** + * The implementation of {@link ValueProvider}. + * + * @author Jan Czogalla + * @since 9.3 + */ +public class ValueProviderImpl implements ValueProvider { + + private String name; + private String type; + private List parameters = new ArrayList<>(); + private Map parameterMap = new TreeMap<>(); + + /** + * Minimal constructor + */ + @JsonCreator + public ValueProviderImpl(@JsonProperty(value = "name", required = true) String name, + @JsonProperty(value = "type", required = true) String type) { + this(name, type, null); + } + + /** + * Full constructor + */ + public ValueProviderImpl(String name, String type, List parameters) { + setName(name); + setType(type); + setParameters(parameters); + } + + @Override + public String getName() { + return name; + } + + /** + * Sets the name of this value provider. Must be neither {@code null} nor empty. + */ + private void setName(String name) { + this.name = ValidationUtil.requireNonEmptyString(name, "name"); + } + + @Override + public String getType() { + return type; + } + + /** + * Sets the type of this value provider. Must be neither {@code null} nor empty. + */ + private void setType(String type) { + this.type = ValidationUtil.requireNonEmptyString(type, "type"); + } + + @Override + public List getParameters() { + return new ArrayList<>(parameters); + } + + /** + * Sets the list of parameters of this value provider. Can be either {@code null} or empty. + */ + private void setParameters(List parameters) { + this.parameters = ValidationUtil.noDuplicatesAllowed(ValidationUtil.stripToEmptyList(parameters), + ValueProviderParameter.UNIQUE_NAME_COMPARATOR, "parameters"); + parameterMap.clear(); + } + + @Override + @JsonIgnore + public Map getParameterMap() { + ensureParameterMap(); + return new TreeMap<>(parameterMap); + } + + private synchronized void ensureParameterMap() { + if (parameterMap.isEmpty() && !parameters.isEmpty()) { + parameters.forEach(p -> parameterMap.put(p.getName(), p)); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ValueProviderImpl that = (ValueProviderImpl) o; + return Objects.equals(name, that.name) && + Objects.equals(type, that.type) && + CollectionUtils.isEqualCollection(parameters, that.parameters); + } + + @Override + public int hashCode() { + return Objects.hash(name, type, parameters); + } + + @Override + public String toString() { + String parameterString = ""; + if (!parameters.isEmpty()) { + parameterString = "\n" + parameters.stream().map(Object::toString).collect(Collectors.joining("\n")); + } + return "Value provider " + name + " of type " + type + parameterString; + } +} diff --git a/src/main/java/com/rapidminer/connection/valueprovider/ValueProviderParameter.java b/src/main/java/com/rapidminer/connection/valueprovider/ValueProviderParameter.java new file mode 100644 index 000000000..cd669c10a --- /dev/null +++ b/src/main/java/com/rapidminer/connection/valueprovider/ValueProviderParameter.java @@ -0,0 +1,71 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.valueprovider; + +import java.util.Comparator; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** + * Interface for a simple key/value pair that could be encrypted. The only mutable part is the value. + *

+ * This interface has a Jackson implementation: {@link ValueProviderParameterImpl} + * + * @author Jan Czogalla + * @since 9.3 + */ +@JsonDeserialize(as = ValueProviderParameterImpl.class) +@JsonPropertyOrder(value = {"name", "encrypted", "enabled", "value"}) +public interface ValueProviderParameter { + + /** + * A comparator to test for uniqueness. While {@link #equals(Object)} might be used for general purposes, + * parameters must be unique by name for all intends and purposes. + */ + Comparator UNIQUE_NAME_COMPARATOR = Comparator.comparing(ValueProviderParameter::getName); + + /** Get the name of this parameter */ + String getName(); + + /** Get the value of this parameter */ + String getValue(); + + /** + * Set the value of this parameter; can be set to {@code null}. + * + * @param value + * the value; will be stripped to {@code null} + */ + void setValue(String value); + + /** Whether this parameter is encrypted */ + boolean isEncrypted(); + + /** Whether this parameter is enabled */ + boolean isEnabled(); + + /** + * Set the enabled status of this parameter. + * + * @param enabled + * whether the parameter is enabled + */ + void setEnabled(boolean enabled); +} diff --git a/src/main/java/com/rapidminer/connection/valueprovider/ValueProviderParameterImpl.java b/src/main/java/com/rapidminer/connection/valueprovider/ValueProviderParameterImpl.java new file mode 100644 index 000000000..b6dcdbe5c --- /dev/null +++ b/src/main/java/com/rapidminer/connection/valueprovider/ValueProviderParameterImpl.java @@ -0,0 +1,220 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.valueprovider; + +import java.io.IOException; +import java.security.Key; +import java.util.Objects; +import java.util.logging.Level; + +import org.apache.commons.lang.StringUtils; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.rapidminer.tools.ValidationUtil; +import com.rapidminer.tools.LogService; +import com.rapidminer.tools.cipher.CipherException; +import com.rapidminer.tools.cipher.CipherTools; +import com.rapidminer.tools.cipher.KeyGeneratorTool; + +/** + * The implementation of {@link ValueProviderParameter}. This is a (enabled or disabled and possibly encrypted) + * key/value pair, where only the {@code value} is mutable and the {@code name} is mandatory. + *

+ * When written to Json using an {@link com.fasterxml.jackson.databind.ObjectMapper ObjectMapper} + * and this parameter is flagged as encrypted, the {@code value} will be encrypted + * with the normal RapidMiner encryption method. + * + * @author Jan Czogalla + * @since 9.3 + */ +public class ValueProviderParameterImpl implements ValueProviderParameter { + + /** + * Mix-In that allows to serialize "value" unencrypted + */ + public abstract class UnencryptedValueMixIn { + @JsonIgnore abstract String getJsonValue(); + @JsonIgnore abstract void setJsonValue(String value); + @JsonGetter("value") abstract String getValue(); + @JsonSetter("value") abstract String setValue(); + } + + private String name; + private boolean encrypted; + private boolean enabled = true; + private String value; + + /** Minimal constructor */ + @JsonCreator + public ValueProviderParameterImpl(@JsonProperty(value = "name", required = true) String name) { + this(name, null, false); + } + + /** Key/value constructor. {@code value} cannot be {@code null} or empty here. */ + public ValueProviderParameterImpl(String name, String value) { + this(name, ValidationUtil.requireNonEmptyString(value, "value"), false); + } + + /** Constructor for enabled parameters. Only {@code name} is mandatory here. */ + public ValueProviderParameterImpl(String name, String value, boolean encrypted) { + this(name, value, encrypted, true); + } + + /** Full constructor. Only {@code name} is mandatory here. */ + public ValueProviderParameterImpl(String name, String value, boolean encrypted, boolean enabled) { + setName(name); + setEncrypted(encrypted); + setEnabled(enabled); + setValue(value); + } + + @Override + public String getName() { + return name; + } + + /** Sets the name of this parameter. Must be neither {@code null} nor empty. */ + private void setName(String name) { + this.name = ValidationUtil.requireNonEmptyString(name, "name"); + } + + @Override + public String getValue() { + return value; + } + + /** + * Json specific getter for the value. Will simply return the value if this parameter is not encrypted. + * If this parameter is encrypted, will return the encrypted value using the {@link Key} defined per Studio user. + * If a key cannot be found, or encrypting does not work, will return {@code null} to prevent leaking the value. + */ + @JsonGetter(value = "value") + private String getJsonValue() { + if (!encrypted || getValue() == null) { + return getValue(); + } + Key key; + try { + key = KeyGeneratorTool.getUserKey(); + } catch (IOException e) { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.connection.encryption.could_not_retrieve_key", e); + return null; + } + try { + return CipherTools.encrypt(getValue(), key); + } catch (CipherException e) { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.connection.encryption.could_not_encrypt", e); + return null; + } + } + + /** Sets the value of this parameter. Can be either {@code null} or empty. */ + @Override + public void setValue(String value) { + this.value = StringUtils.stripToNull(value); + } + + /** + * Json specific setter for the value. Will simply set the value if this parameter is not encrypted. + * If this parameter is encrypted, will try to decrypt the value using the {@link Key} defined per Studio user. + * If a key cannot be found, or decrypting does not work, will set the value to {@code null}. + */ + @JsonSetter(value = "value") + private void setJsonValue(String value) { + value = StringUtils.stripToNull(value); + if (!encrypted || value == null) { + setValue(value); + return; + } + Key key; + try { + key = KeyGeneratorTool.getUserKey(); + } catch (IOException e) { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.connection.encryption.could_not_retrieve_key", e); + setValue(null); + return; + } + try { + setValue(CipherTools.decrypt(value, key)); + } catch (CipherException e) { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.connection.encryption.could_not_decrypt", e); + setValue(null); + } + } + + @Override + public boolean isEncrypted() { + return encrypted; + } + + /** + * Sets this parameter to be encrypted. This is only used during creation, either programmatically or when parsing Json. + * To ensure that encrypted values are decrypted, and since the order inside a Json string cannot be guaranteed, + * this will try to decrypt the value if it was already set before. + */ + private void setEncrypted(boolean encrypted) { + if (this.encrypted == encrypted) { + return; + } + this.encrypted = encrypted; + if (encrypted && value != null) { + // decrypt value if the Json order was different than expected + setJsonValue(value); + } + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ValueProviderParameterImpl that = (ValueProviderParameterImpl) o; + return encrypted == that.encrypted && + enabled == that.enabled && + Objects.equals(name, that.name) && + Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(name, encrypted, enabled, value); + } + + @Override + public String toString() { + return name + ": " + (encrypted ? "(encrypted)" : value) + (enabled ? "" : " - disabled"); + } +} diff --git a/src/main/java/com/rapidminer/connection/valueprovider/handler/BaseValueProviderHandler.java b/src/main/java/com/rapidminer/connection/valueprovider/handler/BaseValueProviderHandler.java new file mode 100644 index 000000000..b3eccb0f4 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/valueprovider/handler/BaseValueProviderHandler.java @@ -0,0 +1,143 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.valueprovider.handler; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.ConnectionInformationSerializer; +import com.rapidminer.connection.util.TestExecutionContext; +import com.rapidminer.connection.util.TestResult; +import com.rapidminer.connection.util.TestResult.ResultType; +import com.rapidminer.connection.util.ValidationResult; +import com.rapidminer.connection.valueprovider.ValueProvider; +import com.rapidminer.connection.valueprovider.ValueProviderImpl; +import com.rapidminer.connection.valueprovider.ValueProviderParameter; +import com.rapidminer.tools.ValidationUtil; + +/** + * Simple base implementation for {@link ValueProviderHandler}. Subclasses only have to implement + * {@link #injectValues(ValueProvider, java.util.Map, com.rapidminer.operator.Operator, com.rapidminer.connection.ConnectionInformation) + * injectValues(ValueProvider, Map, Operator, ConnectionConfiguration)}, + * but it is advised to also implement the {@link #test(TestExecutionContext)} method. + * + * @author Jan Czogalla + * @since 9.3 + */ +public abstract class BaseValueProviderHandler implements ValueProviderHandler { + + private static final ObjectWriter writer; + private static final ObjectReader reader; + + static { + ObjectMapper mapper = ConnectionInformationSerializer.getRemoteObjectMapper(); + JavaType listOfVPP = mapper.getTypeFactory().constructCollectionType(List.class, ValueProviderParameter.class); + writer = mapper.writerWithType(listOfVPP); + reader = mapper.reader(listOfVPP); + } + + private final List parameters; + private final String type; + + + /** Simple handler with no parameters */ + protected BaseValueProviderHandler(String type) { + this.type = ValidationUtil.requireNonEmptyString(type, "type"); + this.parameters = Collections.emptyList(); + } + + /** Handler with parameters */ + protected BaseValueProviderHandler(String type, List parameters) { + this.type = ValidationUtil.requireNonEmptyString(type, "type"); + this.parameters = ValidationUtil.requireNonEmptyList(parameters, "parameters"); + } + + /** + * {@inheritDoc} + *

+ * Does nothing by default. + */ + @Override + public void initialize() { + // noop + } + + /** @return {@code true} by default */ + @Override + public boolean isInitialized() { + return true; + } + + /** @return {@code true} if parameters is not empty */ + @Override + public boolean isConfigurable() { + return !parameters.isEmpty(); + } + + @Override + public String getType() { + return type; + } + + @Override + public ValidationResult validate(ValueProvider object) { + return validate(object, null); + } + + @Override + public ValidationResult validate(ValueProvider object, ConnectionInformation information) { + if (object == null) { + return ValidationResult.nullable(); + } + return ValidationResult.success(ValidationResult.I18N_KEY_SUCCESS); + } + + @Override + public TestResult test(TestExecutionContext object) { + if (object == null || object.getSubject() == null) { + return TestResult.nullable(); + } + return new TestResult(ResultType.NOT_SUPPORTED, TestResult.I18N_KEY_NOT_IMPLEMENTED, null); + } + + @Override + public List getParameters() { + if (parameters.isEmpty()) { + return Collections.emptyList(); + } + try { + // create a deep copy of the parameter list (including potential default values) using Jackson + // a new value provider will start with such a deep copy, see base implementation of createNewProvider(String) + return reader.readValue(writer.writeValueAsBytes(parameters)); + } catch (IOException e) { + return Collections.emptyList(); + } + } + + @Override + public ValueProvider createNewProvider(String name) { + return new ValueProviderImpl(name, getType(), getParameters()); + } +} diff --git a/src/main/java/com/rapidminer/connection/valueprovider/handler/ChainingValueProviderHandler.java b/src/main/java/com/rapidminer/connection/valueprovider/handler/ChainingValueProviderHandler.java new file mode 100644 index 000000000..443eeefdd --- /dev/null +++ b/src/main/java/com/rapidminer/connection/valueprovider/handler/ChainingValueProviderHandler.java @@ -0,0 +1,207 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.valueprovider.handler; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.logging.Level; +import java.util.stream.Collectors; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.configuration.ConnectionConfiguration; +import com.rapidminer.tools.ValidationUtil; +import com.rapidminer.connection.valueprovider.ValueProvider; +import com.rapidminer.connection.valueprovider.ValueProviderParameter; +import com.rapidminer.connection.valueprovider.ValueProviderParameterImpl; +import com.rapidminer.operator.Operator; +import com.rapidminer.parameter.ParameterTypeEnumeration; +import com.rapidminer.tools.LogService; + +/** + * Helps with injecting values into other value providers. Works on a single parameter that is a list separated by + * "{@value ParameterTypeEnumeration#SEPERATOR_CHAR}". This kind of {@link ValueProvider} should be created automatically + * and not by the user. Value providers of this type will always return an {@link Collections#emptyMap() empty map}, + * since they only update the value providers they are concerned with. This implies that only a cloned {@link ConnectionConfiguration} + * should be used when injecting values. + *

+ * To represent a tree of injections, create multiple chains that overlap at the ends and insert them in the right order. + * Chain VPs should preferably be added as the first VPs. + * + * @since 9.3 + * @author Jan Czogalla + */ +public class ChainingValueProviderHandler extends BaseValueProviderHandler { + + /** The type that this handler can process */ + public static final String TYPE = "chaining"; + /** The parameter key that indicates the list of chained provider names */ + public static final String PARAMETER_CHAINED_VPS = "value_providers"; + + private static final ChainingValueProviderHandler INSTANCE = new ChainingValueProviderHandler(); + + private ChainingValueProviderHandler() { + super(TYPE, Collections.singletonList(new ValueProviderParameterImpl(PARAMETER_CHAINED_VPS))); + } + + public static ChainingValueProviderHandler getInstance() { + return INSTANCE; + } + + /** + * Prepares the referenced value providers if they can be found in the given {@link ConnectionConfiguration}. + * Will always return an empty map and ignore the injectables parameter. + */ + @Override + public Map injectValues(ValueProvider vp, Map injectables, Operator operator, ConnectionInformation connection) { + if (!isValid(vp)) { + return Collections.emptyMap(); + } + ConnectionConfiguration configuration = connection.getConfiguration(); + Set chainedList = findChainedProviders(vp, configuration.getValueProviderMap()); + ValueProvider prev = null; + for (ValueProvider next : chainedList) { + if (prev != null) { + Map vpInjectables = next.getParameters().stream() + .filter(p -> p.getValue() == null && p.isEnabled()) + .map(ValueProviderParameter::getName) + .collect(Collectors.toMap(n -> n, n -> n, (a, b) -> b, LinkedHashMap::new)); + String type = prev.getType(); + if (!ValueProviderHandlerRegistry.getInstance().isTypeKnown(type)) { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.connection.injection.missing_value_provider", type); + return Collections.emptyMap(); + } + ValueProviderHandler handler = ValueProviderHandlerRegistry.getInstance().getHandler(type); + Map injected = handler.injectValues(prev, vpInjectables, operator, connection); + next.getParameters().stream().filter(p -> injected.containsKey(p.getName())) + .forEach(p -> p.setValue(injected.get(p.getName()))); + } + prev = next; + } + return Collections.emptyMap(); + } + + /** Creates a new provider from the given names of value providers. */ + public ValueProvider createNewProvider(String name, List vpNames) { + vpNames = ValidationUtil.requireNonEmptyList(vpNames, "value provider names"); + ValueProvider provider = createNewProvider(name); + String value = ParameterTypeEnumeration.transformEnumeration2String(vpNames); + ValueProviderParameter parameter = provider.getParameterMap().get(PARAMETER_CHAINED_VPS); + if (parameter != null) { + parameter.setValue(value); + } + return provider; + } + + /** + * Get the list of chained {@link ValueProvider ValueProviders} in the appropriate order. Will return an empty set if + *

    + *
  1. the {@link ValueProvider} to check is not of type {@value #TYPE}
  2. + *
  3. none or only one value providers listed or
  4. + *
  5. nonexistent value providers listed or
  6. + *
  7. duplicates listed
  8. + *
+ * + * @param chaining + * the provider to be presumed of type {@value #TYPE} + * @param allProviders + * a map of all available providers + * @return the ordered set of providers or an empty set; never {@code null} + */ + public Set findChainedProviders(ValueProvider chaining, Map allProviders) { + if (!isValid(chaining)) { + return new LinkedHashSet<>(); + } + List names = findChainedNames(chaining); + if (names.size() <= 1) { + // no chain, no gain + return new LinkedHashSet<>(); + } + Set chainedProviders = names.stream().map(allProviders::get) + .filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new)); + if (chainedProviders.size() < names.size()) { + chainedProviders.clear(); + } + return chainedProviders; + } + + /** + * Returns a sorted list of value providers, using the given name/value provider map. First this finds all chaining + * value providers. If there are none, simply the list of all other value providers is returned. Else the chaining + * value providers will be sorted according to the dependencies of their chained providers. + *

+ * The returned list has the chaining value providers in the front, followed by the other providers. The latter will + * keep the order in which they are presented to this method. + *

+ * The chaining providers will be sorted as specified above, if they don't contain loops and have only single dependencies. + * An invalid combination of chains would be a -> c, b -> c for example. + * + * @param providerMap + * the map of names to providers + * @return a sorted list of value providers + */ + public List sortValueProviders(Map providerMap) { + Map> chainsAndNormal = providerMap.values().stream() + .collect(Collectors.partitioningBy(ChainingValueProviderHandler::isValid)); + Map> dependencies = new HashMap<>(); + List chains = chainsAndNormal.get(true); + if (chains.isEmpty()) { + return chainsAndNormal.get(false); + } + // find the final VP name for each chain and collect all preceding names as dependencies + Map lastSegmentToChain = chains.stream().collect(Collectors.toMap(vp -> { + List names = findChainedNames(vp); + String lastSegment = names.remove(names.size() - 1); + dependencies.put(lastSegment, new HashSet<>(names)); + return lastSegment; + }, ValueProvider::getName, (a, b) -> a)); + List sortedChains = Collections.emptyList(); + // make sure there is no duplicate chain end! + if (lastSegmentToChain.size() == chains.size()) { + sortedChains = ValidationUtil.dependencySortEmptyListForLoops(dependencies::get, dependencies.keySet()); + } + if (sortedChains.isEmpty()) { + chains.addAll(chainsAndNormal.get(false)); + return chains; + } + // transfer sorting to actual chained value providers + List sortedProviders = sortedChains.stream() + // get corresponding chain provider name for each "normal" VP; ignore those that don't represent a chain + .map(lastSegmentToChain::get).filter(Objects::nonNull) + // get actual chain provider + .map(providerMap::get).collect(Collectors.toList()); + sortedProviders.addAll(chainsAndNormal.get(false)); + return sortedProviders; + } + + private List findChainedNames(ValueProvider chaining) { + return ParameterTypeEnumeration.transformString2List(chaining.getParameterMap().get(PARAMETER_CHAINED_VPS).getValue()); + } + + /** Checks whether the value provider is valid. Checks for {@code null}, type and parameter existence. */ + private static boolean isValid(ValueProvider vp) { + return vp != null && TYPE.equals(vp.getType()) && vp.getParameterMap().get(PARAMETER_CHAINED_VPS) != null; + } +} diff --git a/src/main/java/com/rapidminer/connection/valueprovider/handler/MacroValueProviderGUI.java b/src/main/java/com/rapidminer/connection/valueprovider/handler/MacroValueProviderGUI.java new file mode 100644 index 000000000..2a7827cee --- /dev/null +++ b/src/main/java/com/rapidminer/connection/valueprovider/handler/MacroValueProviderGUI.java @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.valueprovider.handler; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.DefaultValueProviderGUI; +import com.rapidminer.connection.valueprovider.ValueProvider; + + +/** + * GUI for the macro value provider + * + * @author Jonas Wilms-Pfau + * @since 9.3 + */ +public class MacroValueProviderGUI extends DefaultValueProviderGUI { + + @Override + public String getCustomLabel(CustomLabel key, ValueProvider provider, ConnectionInformation connection, String group, String parameterKey, Object... args) { + return super.getCustomLabel(key, provider, connection, group, parameterKey, MacroValueProviderHandler.getPrefix(provider) + parameterKey); + } +} diff --git a/src/main/java/com/rapidminer/connection/valueprovider/handler/MacroValueProviderHandler.java b/src/main/java/com/rapidminer/connection/valueprovider/handler/MacroValueProviderHandler.java new file mode 100644 index 000000000..ab12ba1af --- /dev/null +++ b/src/main/java/com/rapidminer/connection/valueprovider/handler/MacroValueProviderHandler.java @@ -0,0 +1,159 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.valueprovider.handler; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.logging.Level; + +import org.apache.commons.lang.StringUtils; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.valueprovider.ValueProvider; +import com.rapidminer.connection.valueprovider.ValueProviderParameter; +import com.rapidminer.connection.valueprovider.ValueProviderParameterImpl; +import com.rapidminer.operator.Operator; +import com.rapidminer.tools.LogService; + + +/** + * Extracts the required keys out of the macro context of the current process. + *

+ * Use {@link #createNewProvider(String name, String prefix)} to allow for multiple connections of the same type in a single process. + * The macro key is either just "key" or "prefix{@value PARAMETER_PREFIX_SEPARATOR}key" + *

+ * + * @since 9.3 + * @author Jonas Wilms-Pfau + */ +public final class MacroValueProviderHandler extends BaseValueProviderHandler { + + /** + * The singleton instance + */ + private static final MacroValueProviderHandler INSTANCE = new MacroValueProviderHandler(); + + /** + * Configuration prefix parameter name, might be empty + */ + public static final String PARAMETER_PREFIX = "prefix"; + + /** + * Separator char used between prefix and key + */ + public static final String PARAMETER_PREFIX_SEPARATOR = "_"; + + /** + * Get the instance of this singleton + */ + public static MacroValueProviderHandler getInstance() { + return INSTANCE; + } + + /** + * Type of this value provider + */ + public static final String TYPE = "macro_value_provider"; + + /** + * Creates a new MacroValueProviderHandler + */ + private MacroValueProviderHandler() { + super(TYPE, Collections.singletonList(new ValueProviderParameterImpl(PARAMETER_PREFIX))); + } + + @Override + public Map injectValues(ValueProvider vp, Map injectables, Operator operator, ConnectionInformation connection) { + if (!isValid(vp, operator) || injectables == null || injectables.isEmpty()) { + return Collections.emptyMap(); + } + + String prefix = getPrefix(vp); + Map result = new LinkedHashMap<>(); + for (Entry entry : injectables.entrySet()) { + String fullKey = entry.getKey(); + String needed = entry.getValue(); + String value = null; + String key = prefix + needed; + try { + value = operator.getProcess().getMacroHandler().getMacro(key, operator); + } catch (Exception e) { + // this can only happen with detached operators + LogService.log(LogService.getRoot(), Level.WARNING, e, "com.rapidminer.connection.valueprovider.handler.MacroValueProviderHandler.retrieval_failed", key, vp.getName()); + } + if (value != null) { + result.put(fullKey, value); + } else { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.connection.valueprovider.handler.MacroValueProviderHandler.macro_not_found", key); + } + } + + return result; + } + + /** + * Creates a new ValueProvider with the given name, this handler's type and a custom parameter prefix. + * + * @param name The name of the provider + * @param prefix The prefix used for the macro + * @return the new value provider + */ + public ValueProvider createNewProvider(String name, String prefix) { + ValueProvider provider = createNewProvider(name); + ValueProviderParameter prefixParam = provider.getParameterMap().get(PARAMETER_PREFIX); + if (prefixParam != null) { + prefixParam.setValue(prefix); + } + return provider; + } + + /** + * Verifies the given value provider, creates a log entry if the operator context is missing + * + * @param vp + * the value provider to check + * @param context + * the operator that gives context to the value provider + * @return {@code true} if valid + */ + private static boolean isValid(ValueProvider vp, Operator context) { + if (vp != null && context == null) { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.connection.valueprovider.handler.MacroValueProviderHandler.no_context", vp.getName()); + } + return vp != null && TYPE.equals(vp.getType()) && context != null; + } + + /** + * Returns the prefix for a {@link ValueProvider} + * + * @param provider + * the value provider + * @return the prefix, or an empty string + */ + static String getPrefix(ValueProvider provider) { + ValueProviderParameter prefixParam = provider.getParameterMap().get(PARAMETER_PREFIX); + if (prefixParam == null) { + return ""; + } + String prefix = StringUtils.trimToEmpty(prefixParam.getValue()); + return !prefix.isEmpty() ? prefix + PARAMETER_PREFIX_SEPARATOR : prefix; + } +} diff --git a/src/main/java/com/rapidminer/connection/valueprovider/handler/ValueProviderHandler.java b/src/main/java/com/rapidminer/connection/valueprovider/handler/ValueProviderHandler.java new file mode 100644 index 000000000..2b4d74506 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/valueprovider/handler/ValueProviderHandler.java @@ -0,0 +1,93 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.valueprovider.handler; + +import java.util.List; +import java.util.Map; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.util.GenericHandler; +import com.rapidminer.connection.util.TestExecutionContext; +import com.rapidminer.connection.util.ValidationResult; +import com.rapidminer.connection.valueprovider.ValueProvider; +import com.rapidminer.connection.valueprovider.ValueProviderParameter; +import com.rapidminer.operator.Operator; + + +/** + * An interface for handler/factory for {@link ValueProvider ValueProviders}. Implementations provide the possibility to + * create a key/value map of injected values, as well as creating a new {@link ValueProvider}. They can be registered using + * {@link ValueProviderHandlerRegistry#registerHandler ValueProviderHandlerRegistry.registerHandler(ValueProviderHandler)}. + * + * @author Jan Czogalla + * @since 9.3 + */ +public interface ValueProviderHandler extends GenericHandler { + + /** Whether providers created by this handler have parameters */ + boolean isConfigurable(); + + /** + * Returns a list of {@link ValueProviderParameter ValueProviderParameters} that can be found in each {@link ValueProvider} + * created by this handler. Implementations should return a deep copy of their default parameters here. + */ + List getParameters(); + + /** + * Creates a new {@link ValueProvider} with the given name, this handler's type and a copy of the default parameters. + * + * @param name + * the name of the new provider; must be neither {@code null} nor empty + * @see #getParameters() + */ + ValueProvider createNewProvider(String name); + + /** + * Returns a key/value map of values that can be injected, limited to the provided map of injectable keys. The + * {@code injectables} are mapping fully qualified keys to the parameter key. The resulting map should have the same + * sort order as the incoming map and should have the fully qualified parameter key as the map key. + * + * @param vp + * the value provider to use for injection; must not be {@code null} + * @param injectables + * the map of needed injectable values; must not be {@code null} + * @param operator + * the operator that gives context for the value provider; can be {@code null} + * @param connection + * the connection this value provider belongs to; should not be {@code null} + * @return the map with the same order of keys as the given injectables map, must never be {@code null} + */ + Map injectValues(ValueProvider vp, Map injectables, Operator operator, ConnectionInformation connection); + + /** + * Validates the given handled object. This can be used to check if all parameters have sensible values. This + * should only take a very small amount of time as opposed to {@link #test(TestExecutionContext)} which might run a longer + * operation. Should return a {@link com.rapidminer.connection.util.TestResult.ResultType#NONE ResultType.NONE} + * result if the object is {@code null} + *

+ * This method is called instead of {@link #validate(Object)} from the UI. + * + * @param object + * the object to be validated + * @param information + * the information this value provider is used in + * @see ValidationResult + */ + ValidationResult validate(ValueProvider object, ConnectionInformation information); +} diff --git a/src/main/java/com/rapidminer/connection/valueprovider/handler/ValueProviderHandlerRegistry.java b/src/main/java/com/rapidminer/connection/valueprovider/handler/ValueProviderHandlerRegistry.java new file mode 100644 index 000000000..e9d4c79c1 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/valueprovider/handler/ValueProviderHandlerRegistry.java @@ -0,0 +1,363 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.valueprovider.handler; + +import static com.rapidminer.connection.valueprovider.handler.ValueProviderUtils.PLACEHOLDER_INDICATOR; +import static com.rapidminer.connection.valueprovider.handler.ValueProviderUtils.PLACEHOLDER_OPENING; +import static com.rapidminer.connection.valueprovider.handler.ValueProviderUtils.PLACEHOLDER_PREFIX; +import static com.rapidminer.connection.valueprovider.handler.ValueProviderUtils.PLACEHOLDER_SUFFIX; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.apache.commons.collections4.CollectionUtils; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.configuration.ConfigurationParameter; +import com.rapidminer.connection.configuration.ConnectionConfiguration; +import com.rapidminer.connection.configuration.PlaceholderParameter; +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.connection.util.GenericHandlerRegistry; +import com.rapidminer.connection.util.GenericRegistrationEventListener; +import com.rapidminer.connection.valueprovider.ValueProvider; +import com.rapidminer.operator.Operator; +import com.rapidminer.tools.LogService; +import com.rapidminer.tools.ValidationUtil; +import com.rapidminer.tools.usagestats.ActionStatisticsCollector; + + +/** + * The registry for {@link ValueProviderHandler ValueProviderHandlers}. Handlers can be registered and unregistered, + * searched by type and used to inject values for a given {@link ConnectionConfiguration}. + *

+ * Listeners can be added to be notified for (un)registration events. + * + * @author Jan Czogalla + * @since 9.3 + */ +public final class ValueProviderHandlerRegistry extends GenericHandlerRegistry { + + public static final String PARAMETER_ID = "ID"; + public static final String PARAMETER_NAME = "NAME"; + public static final String PARAMETER_TYPE = "TYPE"; + public static final String REMOTE = "REMOTE"; + + /** Hide these Value Provider types from the user */ + private static final Collection INVISIBLE_TYPES_REMOTE = Collections.singletonList(ChainingValueProviderHandler.TYPE); + private static final Collection INVISIBLE_TYPES = CollectionUtils.union(Collections.singletonList("remote_repository:rapidminer_vault"), INVISIBLE_TYPES_REMOTE); + + + /** + * A simple helper class for place holder injection; can be used both for a constant or a placeholder. + * + * @author Jan Czogalla + */ + private static final class PlaceholderWrapper implements Function, String>, Supplier { + + private static final PlaceholderWrapper EMPTY = new PlaceholderWrapper(); + + private String key; + private String constant; + + @Override + public String apply(Map parameters) { + return Objects.toString(key == null ? constant : parameters.get(key), ""); + } + + @Override + public String get() { + return key; + } + + /** Create a placeholder instance */ + private static PlaceholderWrapper withKey(String key) { + PlaceholderWrapper phw = new PlaceholderWrapper(); + phw.key = key; + return phw; + } + + /** Create a constant instance */ + private static PlaceholderWrapper constant(String constant) { + PlaceholderWrapper phw = new PlaceholderWrapper(); + phw.constant = constant; + return phw; + } + + /** Create an empty instance */ + private static PlaceholderWrapper empty() { + return EMPTY; + } + } + + private static final ValueProviderHandlerRegistry INSTANCE = new ValueProviderHandlerRegistry(); + + static { + // Register default handlers + INSTANCE.registerHandler(MacroValueProviderHandler.getInstance()); + INSTANCE.registerHandler(ChainingValueProviderHandler.getInstance()); + } + + /** + * Finds placeholders in the form of %{.*}; makes sure that nested placeholders are found first + * (e.g. %{foo%{bar}test} will find %{bar}, but not the surrounding placeholder expression. + * Nested placeholders are not allowed or at least not handled (i.e. no recursive resolution). + */ + private static final String PLACEHOLDER_REGEX = Pattern.quote(PLACEHOLDER_PREFIX) + + "((?:[^" + Pattern.quote(PLACEHOLDER_INDICATOR) + "]|" + + Pattern.quote(PLACEHOLDER_INDICATOR) + "(?!" + Pattern.quote(PLACEHOLDER_OPENING) + "))*?)" + + Pattern.quote(PLACEHOLDER_SUFFIX); + public static final Pattern PLACEHOLDER_PATTERN = Pattern.compile(PLACEHOLDER_REGEX); + + /** Singleton class, no instantiation allowed except for internal purpose */ + private ValueProviderHandlerRegistry() {} + + /** Get the instance of this singleton */ + public static ValueProviderHandlerRegistry getInstance() { + return INSTANCE; + } + + /** + * Returns a map of all actual parameter/value pairs. If only injected values are requested, all static parameters + * (i.e. those that are neither injected by a {@link ValueProvider} nor contain placeholders) will not be present in + * the returned map. Static parameters that will always be present are {@value #PARAMETER_ID}, {@value #PARAMETER_NAME} + * and {@value #PARAMETER_TYPE}, representing the respective fields of the {@link ConnectionConfiguration}. + *

+ * This will find all {@link ConfigurationParameter ConfigurationParameters} and + * {@link PlaceholderParameter PlaceholderConfigurationParameters} that are marked as injected, + * as well as parameters that contain {@code %{[group.]name}} style placeholders and will try + * to find injections for these keys/placeholders. + *

+ * Parameters that are marked as injected will be filled in using the {@link ValueProvider ValueProviders} of the + * {@link ConnectionConfiguration} first, after that placeholders will be injected using the static and already injected + * parameter values. + *

+ * The order of value providers and placeholder injection are calculated using {@link ChainingValueProviderHandler#sortValueProviders(Map)} + * and the actual dependencies of the placeholders. If there are circular dependencies, nothing might be injected. + *

+ * Placeholders may reference other parameter names inside their own group or fully qualified keys ([@code group.name}) + * for parameters in other groups. + * + * @param connection + * the connection whose parameters should be injected + * @param operator + * the operator needed for context, may be {@code null} + * @param onlyInjected + * if only the injected values (value provider/placeholder) should be returned + */ + public Map injectValues(ConnectionInformation connection, Operator operator, boolean onlyInjected) { + if (connection == null) { + // no configuration, no injection + return null; + } + ConnectionConfiguration configuration = connection.getConfiguration(); + Map valueProviders = configuration.getValueProviderMap(); + Matcher matcher = PLACEHOLDER_PATTERN.matcher(""); + Map keys = configuration.getKeyMap(); + keys.putAll(configuration.getPlaceholderKeyMap()); + Map staticParameters = new TreeMap<>(); + Map> keyPlaceholderMap = new LinkedHashMap<>(); + Map> injectorKeyMap = new LinkedHashMap<>(valueProviders.size()); + ChainingValueProviderHandler.getInstance().sortValueProviders(valueProviders) + .forEach(vp -> injectorKeyMap.put(vp, new LinkedHashMap<>())); + // separate out static parameters, injected parameters and parameters with placeholders + for (Entry entry : keys.entrySet()) { + String k = entry.getKey(); + ConfigurationParameter parameter = entry.getValue(); + + //ignore disabled parameters + if (!parameter.isEnabled()) { + continue; + } + String parameterKey = parameter.getName(); + + // find parameters that are injected + if (parameter.isInjected()) { + String injectorName = parameter.getInjectorName(); + ValueProvider vp = valueProviders.get(injectorName); + if (vp == null) { + continue; + } + injectorKeyMap.get(vp).put(k, parameterKey); + continue; + } + + String value = parameter.getValue(); + String keyPrefix = k.substring(0, k.length() - parameterKey.length()); + // find out if parameter value contains placeholders + List phws = constructPlaceholders(keyPrefix, value, keys.keySet(), matcher); + if (phws.isEmpty()) { + staticParameters.put(k, value); + } else { + keyPlaceholderMap.put(k, phws); + } + } + + // static parameters (i.e. all parameters without injectables) + // plus generic parameters as ID, name and connection type + staticParameters.put(PARAMETER_ID, configuration.getId()); + staticParameters.put(PARAMETER_NAME, configuration.getName()); + staticParameters.put(PARAMETER_TYPE, configuration.getType()); + if (injectorKeyMap.isEmpty() && keyPlaceholderMap.isEmpty()) { + // nothing to inject, only return static parameters + return onlyInjected ? Collections.emptyMap() : staticParameters; + } + + // check for circular dependencies and sort injectables by dependencies + try { + keyPlaceholderMap = ValidationUtil.dependencySortNoLoops( + (key, phws) -> phws.stream().map(PlaceholderWrapper::get).filter(Objects::nonNull).collect(Collectors.toSet()), + keyPlaceholderMap); + } catch (IllegalArgumentException e) { + // ignore + } + + // inject parameters that are marked as such + Map collectedParameters = new TreeMap<>(staticParameters); + injectorKeyMap.forEach((vp, parameterMap) -> { + String vpType = vp.getType(); + if (isTypeKnown(vpType)) { + Map injectedValues = getHandler(vpType).injectValues(vp, parameterMap, operator, connection); + parameterMap.forEach((key, value) -> { + if (injectedValues == null) { + LogService.getRoot().log(Level.SEVERE, "com.rapidminer.connection.injection.bad_vp_null", vp.getName()); + logInjection(configuration, vpType, key, "bad_vp_null"); + return; + } + + if (!injectedValues.containsKey(key)) { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.connection.injection.value_not_injected", + new Object[]{ConnectionI18N.getParameterName(configuration.getType(), key, value), vp.getName()}); + logInjection(configuration, vpType, key, "value_not_injected"); + } else { + collectedParameters.putIfAbsent(key, injectedValues.get(key)); + logInjection(configuration, vpType, key, ActionStatisticsCollector.ARG_SUCCESS); + } + }); + } else { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.connection.injection.missing_value_provider", vpType); + logInjection(configuration, vpType, "", "missing_value_provider"); + } + }); + + // inject placeholders + keyPlaceholderMap.forEach((key, phws) -> collectedParameters.putIfAbsent(key, + phws.stream().map(phw -> phw.apply(collectedParameters)).collect(Collectors.joining()))); + + if (onlyInjected) { + staticParameters.keySet().forEach(collectedParameters::remove); + } + return collectedParameters; + } + + /** + * Get a list of all the visible {@link ValueProvider} types which can be configured by the user + * + * @param filter + * to get visible {@link ValueProvider} types for remote connections use {@code ValueProviderHandlerRegistry#REMOTE} here + * @return a list of types of configurable ValueProviders + */ + public List getVisibleTypes(String filter) { + final List allTypes = getAllTypes(); + if (REMOTE.equals(filter)) { + allTypes.removeAll(INVISIBLE_TYPES_REMOTE); + } else { + allTypes.removeAll(INVISIBLE_TYPES); + } + return allTypes; + } + + @Override + @SuppressWarnings("unchecked") + protected , L extends G> Class getListenerClass(L listener) { + return (Class) (listener == null || listener instanceof ValueProviderHandlerRegistryListener ? + ValueProviderHandlerRegistryListener.class : listener.getClass()); + } + + @Override + protected String getRegistryType() { + return "value_provider"; + } + + /** + * Extract all placeholders from a parameter's value and resolve them to their fully qualified name. Returns a list + * of {@link PlaceholderWrapper PlaceholderWrappers} to easily transform them to their injected values. + * If nothing needs to be injected, an empty list is returned. + * + * @param keyPrefix + * the prefix of the full key, i.e. group. + * @param value + * the value to check for placeholders + * @param availableKeys + * set of available fully qualified keys + * @param matcher + * the matcher for placeholders + * @return the list of placeholder wrappers; might be empty + */ + private List constructPlaceholders(String keyPrefix, String value, Set availableKeys, Matcher matcher) { + if (value == null) { + return Collections.emptyList(); + } + matcher.reset(value); + int pos = 0; + List phws = new ArrayList<>(); + while (matcher.find(pos)) { + int start = matcher.start(); + // constant part before the first match or after previous match + if (start > pos) { + phws.add(PlaceholderWrapper.constant(value.substring(pos, start))); + } + String placeholder = matcher.group(1); + PlaceholderWrapper wrapper = PlaceholderWrapper.empty(); + if (!placeholder.isEmpty() && (availableKeys.contains(placeholder) + || availableKeys.contains(placeholder = keyPrefix + placeholder))) { + wrapper = PlaceholderWrapper.withKey(placeholder); + } + phws.add(wrapper); + pos = matcher.end(); + } + // constant part at the end, after at least one placeholder was found + if (pos > 0 && pos < value.length()) { + phws.add(PlaceholderWrapper.constant(value.substring(pos))); + } + return phws; + } + + /** + * Logs the result of the injection for a connection key. + */ + private void logInjection(ConnectionConfiguration configuration, String vpType, String key, String result) { + ActionStatisticsCollector.INSTANCE.log(ActionStatisticsCollector.TYPE_CONNECTION_INJECTION, + configuration.getType() + ActionStatisticsCollector.ARG_SPACER + vpType, + key + ActionStatisticsCollector.ARG_SPACER + result); + } +} diff --git a/src/main/java/com/rapidminer/connection/valueprovider/handler/ValueProviderHandlerRegistryListener.java b/src/main/java/com/rapidminer/connection/valueprovider/handler/ValueProviderHandlerRegistryListener.java new file mode 100644 index 000000000..ac9ac4b82 --- /dev/null +++ b/src/main/java/com/rapidminer/connection/valueprovider/handler/ValueProviderHandlerRegistryListener.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.valueprovider.handler; + +import com.rapidminer.connection.util.GenericRegistrationEventListener; + +/** + * Marker interface for registration events happening in the {@link ValueProviderHandlerRegistry}. + * + * @author Jan Czogalla + * @since 9.3 + */ +public interface ValueProviderHandlerRegistryListener extends GenericRegistrationEventListener {} diff --git a/src/main/java/com/rapidminer/connection/valueprovider/handler/ValueProviderUtils.java b/src/main/java/com/rapidminer/connection/valueprovider/handler/ValueProviderUtils.java new file mode 100644 index 000000000..9f779253c --- /dev/null +++ b/src/main/java/com/rapidminer/connection/valueprovider/handler/ValueProviderUtils.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General + * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses/. + */ +package com.rapidminer.connection.valueprovider.handler; + +import org.apache.commons.lang.StringUtils; + + +/** + * Some utility methods for dealing with injection of values. + * + * @author Marco Boeck + * @since 9.3.0 + */ +public enum ValueProviderUtils { + ; // no instance + + /** this is the indicator of a placeholder. Placeholders are used to reference values from other fields. */ + public static final String PLACEHOLDER_INDICATOR = "%"; + /** this is the opening bracket of a placeholder. Placeholders are used to reference values from other fields. */ + public static final String PLACEHOLDER_OPENING = "{"; + /** + * this is the prefix of a placeholder. Placeholders are used to reference values from other fields. + * The prefix is comprised of the {@link #PLACEHOLDER_INDICATOR} and the {@link #PLACEHOLDER_OPENING}. + */ + public static final String PLACEHOLDER_PREFIX = PLACEHOLDER_INDICATOR + PLACEHOLDER_OPENING; + /** this is the suffix of a placeholder. Placeholders are used to reference values from other fields. */ + public static final String PLACEHOLDER_SUFFIX = "}"; + + + /** + * Wraps the given string as a placeholder, i.e. "%{value}". + * + * @param key + * the key of the placeholder, must not be {@code null} or empty + * @return the wrapped value + */ + public static String wrapIntoPlaceholder(String key) { + if (StringUtils.trimToNull(key) == null) { + throw new IllegalArgumentException("key must neither be null nor empty!"); + } + + return PLACEHOLDER_PREFIX + key + PLACEHOLDER_SUFFIX; + } + +} diff --git a/src/main/java/com/rapidminer/core/license/ProductLinkRegistry.java b/src/main/java/com/rapidminer/core/license/ProductLinkRegistry.java index c3a99dde3..65d0a982b 100644 --- a/src/main/java/com/rapidminer/core/license/ProductLinkRegistry.java +++ b/src/main/java/com/rapidminer/core/license/ProductLinkRegistry.java @@ -20,15 +20,13 @@ import java.net.MalformedURLException; import java.net.URL; -import java.security.AccessControlException; -import java.security.AccessController; import java.util.HashMap; import java.util.Map; import java.util.Objects; import com.rapidminer.license.StudioLicenseConstants; import com.rapidminer.license.product.Product; -import com.rapidminer.security.PluginSandboxPolicy; +import com.rapidminer.tools.Tools; /** @@ -62,7 +60,7 @@ public enum ProductLinkRegistry { * if the link is not a valid url */ public void register(String productId, String link) { - checkAccess(); + Tools.requireInternalPermission(); Objects.requireNonNull(productId, "productId must not be null"); Objects.requireNonNull(link, "link must not be null"); try { @@ -93,19 +91,4 @@ public String get(String productId, String defaultLink) { return productLinkMap.getOrDefault(productId, defaultLink); } - /** - * Checks if the caller has the {@link PluginSandboxPolicy#RAPIDMINER_INTERNAL_PERMISSION} - * - * @throws UnsupportedOperationException - * if the caller is not signed - */ - private static void checkAccess() { - try { - if (System.getSecurityManager() != null) { - AccessController.checkPermission(new RuntimePermission(PluginSandboxPolicy.RAPIDMINER_INTERNAL_PERMISSION)); - } - } catch (AccessControlException e) { - throw new UnsupportedOperationException("Internal API, cannot be called by unauthorized sources."); - } - } } diff --git a/src/main/java/com/rapidminer/example/table/internal/DoubleAutoSparseChunk.java b/src/main/java/com/rapidminer/example/table/internal/DoubleAutoSparseChunk.java index b653dbd21..c87da25f5 100644 --- a/src/main/java/com/rapidminer/example/table/internal/DoubleAutoSparseChunk.java +++ b/src/main/java/com/rapidminer/example/table/internal/DoubleAutoSparseChunk.java @@ -103,8 +103,8 @@ void setLast(int row, double value) { private void changeToDense(int max) { DoubleAutoChunk dense = new DoubleAutoDenseChunk(chunks, id, ensuredSize, management); - // allow to change back to sparse when default was guessed - if (!testingDefaultValue) { + // allow to change back to sparse when default was guessed except when at the threshold + if (!testingDefaultValue || max == AutoColumnUtils.THRESHOLD_CHECK_FOR_SPARSE - 1) { dense.complete(); } for (int i = 0; i <= max; i++) { diff --git a/src/main/java/com/rapidminer/example/table/internal/IntegerAutoSparseChunk.java b/src/main/java/com/rapidminer/example/table/internal/IntegerAutoSparseChunk.java index fd84ea998..3db5ca45d 100644 --- a/src/main/java/com/rapidminer/example/table/internal/IntegerAutoSparseChunk.java +++ b/src/main/java/com/rapidminer/example/table/internal/IntegerAutoSparseChunk.java @@ -103,8 +103,8 @@ void setLast(int row, double value) { private void changeToDense(int max) { IntegerAutoChunk dense = new IntegerAutoDenseChunk(id, chunks, ensuredSize, management); - // allow to change back to sparse when default was guessed - if (!testingDefaultValue) { + // allow to change back to sparse when default was guessed except when at the threshold + if (!testingDefaultValue || max == AutoColumnUtils.THRESHOLD_CHECK_FOR_SPARSE - 1) { dense.complete(); } for (int i = 0; i <= max; i++) { diff --git a/src/main/java/com/rapidminer/gui/MainFrame.java b/src/main/java/com/rapidminer/gui/MainFrame.java index abf98dd22..0c6c03bb1 100644 --- a/src/main/java/com/rapidminer/gui/MainFrame.java +++ b/src/main/java/com/rapidminer/gui/MainFrame.java @@ -38,6 +38,7 @@ import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; import javax.swing.JToolBar; import javax.swing.SwingUtilities; import javax.swing.Timer; @@ -55,6 +56,7 @@ import com.rapidminer.gui.actions.Actions; import com.rapidminer.gui.actions.AutoWireAction; import com.rapidminer.gui.actions.BrowseAction; +import com.rapidminer.gui.actions.CreateConnectionAction; import com.rapidminer.gui.actions.ExitAction; import com.rapidminer.gui.actions.ExportProcessAction; import com.rapidminer.gui.actions.ImportDataAction; @@ -393,6 +395,8 @@ private static final String getFrameTitle() { public final transient Action VALIDATE_ACTION = new ValidateProcessAction(); public final transient ToggleAction VALIDATE_AUTOMATICALLY_ACTION = new ValidateAutomaticallyAction(); + public final transient Action CREATE_CONNECTION = new CreateConnectionAction(); + private transient JButton runRemoteToolbarButton; public final transient Action NEW_PERSPECTIVE_ACTION = new NewPerspectiveAction(); @@ -476,6 +480,8 @@ private static final String getFrameTitle() { private final JMenu connectionsMenu; + private final JMenu legacyConnectionsMenu; + private final JMenu viewMenu; private final JMenu helpMenu; @@ -799,8 +805,13 @@ public void menuCanceled(MenuEvent e) { // connections menu connectionsMenu = new ResourceMenu("connections"); connectionsMenu.setMargin(menuBarInsets); + connectionsMenu.add(CREATE_CONNECTION); + connectionsMenu.addSeparator(); menuBar.add(connectionsMenu); + // legacy connections menu + legacyConnectionsMenu = new ResourceMenu("legacy_connections"); + // help menu helpMenu = new ResourceMenu("help"); helpMenu.add(TUTORIAL_ACTION); @@ -861,10 +872,18 @@ public void finishInitialization() { // Configurators (if they exist) if (!ConfigurationManager.getInstance().isEmpty()) { + legacyConnectionsMenu.add(MANAGE_CONFIGURABLES_ACTION); + } + + // Add legacy menu (if not empty) + if (legacyConnectionsMenu.getMenuComponentCount() > 0) { connectionsMenu.addSeparator(); - connectionsMenu.add(MANAGE_CONFIGURABLES_ACTION); + connectionsMenu.add(legacyConnectionsMenu); } + // remove duplicated separators + removeDuplicatedSeparators(connectionsMenu); + // add export and exit as last file menu actions fileMenu.addSeparator(); fileMenu.add(EXPORT_ACTION); @@ -1622,6 +1641,14 @@ public JMenu getConnectionsMenu() { return connectionsMenu; } + /** + * This returns the legacy connections menu to change menu entries + * @since 9.3 + */ + public JMenu getLegacyConnectionsMenu() { + return legacyConnectionsMenu; + } + /** * This returns the settings menu to change menu entries. * @@ -1781,4 +1808,23 @@ private boolean doesProcessContainShowstoppers() { // no showstopper return false; } + + /** + * Removes duplicated separators from a JMenu + * + * @param menu the menu + */ + private static void removeDuplicatedSeparators(JMenu menu) { + int separatorCount = 0; + for (Component component : menu.getMenuComponents()) { + if (component instanceof JPopupMenu.Separator) { + separatorCount++; + } else { + separatorCount = 0; + } + if (separatorCount > 1) { + menu.remove(component); + } + } + } } diff --git a/src/main/java/com/rapidminer/gui/MetaDataUpdateQueue.java b/src/main/java/com/rapidminer/gui/MetaDataUpdateQueue.java index edd71cd9b..0ff2cc330 100644 --- a/src/main/java/com/rapidminer/gui/MetaDataUpdateQueue.java +++ b/src/main/java/com/rapidminer/gui/MetaDataUpdateQueue.java @@ -21,11 +21,11 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.WeakHashMap; import java.util.logging.Level; import javax.swing.SwingUtilities; @@ -50,7 +50,8 @@ public class MetaDataUpdateQueue extends UpdateQueue { /** @since 9.2.0 */ static final String REVALIDATE_PROCESS_KEY = "re" + VALIDATE_PROCESS_KEY; /** @since 9.2.0 */ - private static final Map> MD_GENERATION_CHECKERS = Collections.synchronizedMap(new HashMap<>()); + private static final Map> MD_GENERATION_CHECKERS = + Collections.synchronizedMap(new WeakHashMap<>()); private final MainFrame mainFrame; diff --git a/src/main/java/com/rapidminer/gui/OperatorDocLoader.java b/src/main/java/com/rapidminer/gui/OperatorDocLoader.java index 1e6fc9cce..d6db35605 100644 --- a/src/main/java/com/rapidminer/gui/OperatorDocLoader.java +++ b/src/main/java/com/rapidminer/gui/OperatorDocLoader.java @@ -64,6 +64,7 @@ */ public class OperatorDocLoader { + public static final String DEFAULT_IOOBJECT_ICON_NAME = "question_blue.png"; /** * The documentation cache. It is used to cache documentations after reading them for the first * time. @@ -289,7 +290,7 @@ private static String getIconNameForType(Class clazz) { String path = null; String iconName; if (clazz == null) { - iconName = "plug.png"; + iconName = DEFAULT_IOOBJECT_ICON_NAME; } else { iconName = RendererService.getIconName(clazz); } diff --git a/src/main/java/com/rapidminer/gui/OperatorDocToHtmlConverter.java b/src/main/java/com/rapidminer/gui/OperatorDocToHtmlConverter.java index e99c92783..0cefdfdb7 100644 --- a/src/main/java/com/rapidminer/gui/OperatorDocToHtmlConverter.java +++ b/src/main/java/com/rapidminer/gui/OperatorDocToHtmlConverter.java @@ -18,6 +18,8 @@ */ package com.rapidminer.gui; +import static com.rapidminer.gui.OperatorDocLoader.DEFAULT_IOOBJECT_ICON_NAME; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.StringWriter; @@ -541,25 +543,21 @@ public static boolean isParameterOptional(String operatorKey, String parameterNa */ @SuppressWarnings("unchecked") public static String getIconNameForType(String type) { - String iconName; - if (type == null || type.isEmpty()) { - iconName = "plug.png"; - } else { + String iconName = DEFAULT_IOOBJECT_ICON_NAME; + if (type != null && !type.isEmpty()) { Class typeClass; try { typeClass = (Class) Class.forName(type); iconName = RendererService.getIconName(typeClass); if (iconName == null) { - iconName = "plug.png"; + iconName = DEFAULT_IOOBJECT_ICON_NAME; } } catch (ClassNotFoundException e) { LogService.getRoot().finer("Failed to lookup class '" + type + "'. Reason: " + e.getLocalizedMessage()); - iconName = "plug.png"; } } - String path = SwingTools.getIconPath("24/" + iconName); - return path; + return SwingTools.getIconPath("24/" + iconName); } public static String getTagHtmlForKey(String operatorKey) { diff --git a/src/main/java/com/rapidminer/gui/StaticButtonModel.java b/src/main/java/com/rapidminer/gui/StaticButtonModel.java new file mode 100644 index 000000000..24ef15282 --- /dev/null +++ b/src/main/java/com/rapidminer/gui/StaticButtonModel.java @@ -0,0 +1,158 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.gui; + +import static java.awt.event.KeyEvent.VK_UNDEFINED; + +import java.awt.event.ActionListener; +import java.awt.event.ItemListener; +import javax.swing.ButtonGroup; +import javax.swing.ButtonModel; +import javax.swing.event.ChangeListener; + +/** + * A static {@link ButtonModel} that can be used on {@link javax.swing.JRadioButton JRadioButtons} to make them a + * mere indicator. + *

+ * Inspired by + * https://coderanch.com/t/346898/java/Making-radio-buttons-noneditable + * + * @author Jan Czogalla + * @since 9.3 + */ +public class StaticButtonModel implements ButtonModel { + + private boolean selected; + + public StaticButtonModel(boolean selected) { + this.selected = selected; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public String getActionCommand() { + return "static"; + } + + @Override + public int getMnemonic() { + return VK_UNDEFINED; + } + + @Override + public boolean isSelected() { + return selected; + } + + //region noop/default methods + @Override + public boolean isArmed() { + return false; + } + + @Override + public boolean isPressed() { + return false; + } + + @Override + public boolean isRollover() { + return false; + } + + @Override + public void setArmed(boolean b) { + // noop + } + + @Override + public void setSelected(boolean b) { + // noop + } + + @Override + public void setEnabled(boolean b) { + // noop + } + + @Override + public void setPressed(boolean b) { + // noop + } + + @Override + public void setRollover(boolean b) { + // noop + } + + @Override + public void setMnemonic(int key) { + // noop + } + + @Override + public void setActionCommand(String s) { + // noop + } + + @Override + public void setGroup(ButtonGroup group) { + // noop + } + + @Override + public void addActionListener(ActionListener l) { + // noop + } + + @Override + public void removeActionListener(ActionListener l) { + // noop + } + + @Override + public Object[] getSelectedObjects() { + return null; + } + + @Override + public void addItemListener(ItemListener l) { + // noop + } + + @Override + public void removeItemListener(ItemListener l) { + // noop + } + + @Override + public void addChangeListener(ChangeListener l) { + // noop + } + + @Override + public void removeChangeListener(ChangeListener l) { + // noop + } + //endregion +} diff --git a/src/main/java/com/rapidminer/gui/actions/CloseAllResultsExceptCurrentResultAction.java b/src/main/java/com/rapidminer/gui/actions/CloseAllResultsExceptCurrentResultAction.java new file mode 100644 index 000000000..fea31606b --- /dev/null +++ b/src/main/java/com/rapidminer/gui/actions/CloseAllResultsExceptCurrentResultAction.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. +*/ +package com.rapidminer.gui.actions; + +import java.awt.event.ActionEvent; + +import com.rapidminer.gui.MainFrame; +import com.rapidminer.gui.tools.ResourceAction; + + +/** + * An action to close all currently open results except for the result that this action was triggered on. + * + * @author Marco Boeck + * @since 9.3 + */ +public class CloseAllResultsExceptCurrentResultAction extends ResourceAction { + + private final MainFrame mainframe; + private final String dockKey; + + + public CloseAllResultsExceptCurrentResultAction(MainFrame mainframe, String dockKey) { + super(true, "close_all_results_except_current"); + this.mainframe = mainframe; + this.dockKey = dockKey; + } + + @Override + public void loggedActionPerformed(ActionEvent e) { + if (mainframe != null && mainframe.getResultDisplay() != null) { + mainframe.getResultDisplay().clearAllExcept(dockKey); + } + } +} diff --git a/src/main/java/com/rapidminer/gui/actions/CopyStringToClipboardAction.java b/src/main/java/com/rapidminer/gui/actions/CopyStringToClipboardAction.java new file mode 100644 index 000000000..c2edf9306 --- /dev/null +++ b/src/main/java/com/rapidminer/gui/actions/CopyStringToClipboardAction.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. +*/ +package com.rapidminer.gui.actions; + +import java.awt.event.ActionEvent; +import java.util.function.Supplier; + +import com.rapidminer.gui.tools.ResourceAction; +import com.rapidminer.tools.Tools; +import com.rapidminer.tools.ValidationUtil; + + +/** + * The action copies the string given by the supplier to the clipboard. + * + * @author Marco Boeck + * @since 9.3.0 + */ +public class CopyStringToClipboardAction extends ResourceAction { + + private Supplier stringSupplier; + + + /** + * Creates a new {@link CopyStringToClipboardAction} instance. + * + * @param smallIcon + * {@code true} for a 16px icon; {@code false} for a 24px icon + * @param i18nKey + * the i18n key which will be part of the composite key {@code gui.action.{i18nkey}} for the action + * @param stringSupplier + * the supplier which supplies the string when the action is performed, must not be {@code null} + */ + public CopyStringToClipboardAction(boolean smallIcon, String i18nKey, Supplier stringSupplier) { + super(smallIcon, i18nKey); + this.stringSupplier = ValidationUtil.requireNonNull(stringSupplier, "stringSupplier"); + } + + @Override + public void loggedActionPerformed(ActionEvent e) { + Tools.copyStringToClipboard(stringSupplier.get()); + } + +} diff --git a/src/main/java/com/rapidminer/gui/actions/CreateConnectionAction.java b/src/main/java/com/rapidminer/gui/actions/CreateConnectionAction.java new file mode 100644 index 000000000..bb2c88405 --- /dev/null +++ b/src/main/java/com/rapidminer/gui/actions/CreateConnectionAction.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. +*/ +package com.rapidminer.gui.actions; + +import java.awt.event.ActionEvent; + +import com.rapidminer.gui.tools.ResourceAction; + + +/** + * Action to create a new {@link com.rapidminer.connection.ConnectionInformation Connection}. + * + * @author Marco Boeck + * @since 9.3.0 + */ +public class CreateConnectionAction extends ResourceAction { + + + public CreateConnectionAction() { + super("create_connection"); + } + + @Override + public void loggedActionPerformed(ActionEvent e) { + com.rapidminer.repository.gui.actions.CreateConnectionAction.createConnection(null); + } +} diff --git a/src/main/java/com/rapidminer/gui/actions/OpenAction.java b/src/main/java/com/rapidminer/gui/actions/OpenAction.java index 76bfc6e5c..af4d6d5b6 100644 --- a/src/main/java/com/rapidminer/gui/actions/OpenAction.java +++ b/src/main/java/com/rapidminer/gui/actions/OpenAction.java @@ -1,37 +1,40 @@ /** * Copyright (C) 2001-2019 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.gui.actions; import java.awt.event.ActionEvent; import java.io.IOException; - +import java.util.logging.Level; +import javax.swing.JLabel; import javax.swing.SwingUtilities; import com.rapidminer.Process; import com.rapidminer.ProcessLocation; import com.rapidminer.RepositoryProcessLocation; +import com.rapidminer.gui.ApplicationFrame; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.tools.ProgressThread; import com.rapidminer.gui.tools.ResourceAction; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.gui.tools.dialogs.ConfirmDialog; import com.rapidminer.operator.ResultObject; +import com.rapidminer.repository.ConnectionEntry; import com.rapidminer.repository.Entry; import com.rapidminer.repository.IOObjectEntry; import com.rapidminer.repository.MalformedRepositoryLocationException; @@ -39,6 +42,9 @@ import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; import com.rapidminer.repository.gui.RepositoryLocationChooser; +import com.rapidminer.connection.ConnectionInformationContainerIOObject; +import com.rapidminer.repository.gui.actions.EditConnectionAction; +import com.rapidminer.tools.LogService; import com.rapidminer.tools.XMLException; @@ -62,7 +68,9 @@ public void loggedActionPerformed(ActionEvent e) { open(); } - /** Loads the data held by the given entry (in the background) and opens it as a result. */ + /** + * Loads the data held by the given entry (in the background) and opens it as a result. + */ public static void showAsResult(final IOObjectEntry data) { if (data == null) { throw new IllegalArgumentException("data entry must not be null"); @@ -96,14 +104,14 @@ public static void open() { Entry entry = location.locateEntry(); if (entry instanceof ProcessEntry) { open(new RepositoryProcessLocation(location), true); + } else if (entry instanceof ConnectionEntry) { + showConnectionInformationDialog((ConnectionEntry) entry); } else if (entry instanceof IOObjectEntry) { showAsResult((IOObjectEntry) entry); } else { SwingTools.showVerySimpleErrorMessage("no_data_or_process"); } - } catch (MalformedRepositoryLocationException e) { - SwingTools.showSimpleErrorMessage("while_loading", e, locationString, e.getMessage()); - } catch (RepositoryException e) { + } catch (MalformedRepositoryLocationException | RepositoryException e) { SwingTools.showSimpleErrorMessage("while_loading", e, locationString, e.getMessage()); } } @@ -142,17 +150,11 @@ public void run() { if (isCancelled()) { return; } - SwingUtilities.invokeLater(new Runnable() { - - @Override - public void run() { - RapidMinerGUI.getMainFrame().setOpenedProcess(process); - } - }); + SwingUtilities.invokeLater(() -> RapidMinerGUI.getMainFrame().setOpenedProcess(process)); } catch (XMLException ex) { try { RapidMinerGUI.getMainFrame() - .handleBrokenProxessXML(processLocation, processLocation.getRawXML(), ex); + .handleBrokenProxessXML(processLocation, processLocation.getRawXML(), ex); } catch (IOException e) { SwingTools.showSimpleErrorMessage("while_loading", e, processLocation, e.getMessage()); return; @@ -174,8 +176,10 @@ public static void open(String openLocation, boolean showInfo) { Entry entry = location.locateEntry(); if (entry instanceof ProcessEntry) { open(new RepositoryProcessLocation(location), showInfo); + } else if (entry instanceof ConnectionEntry) { + showConnectionInformationDialog((ConnectionEntry) entry); } else if (entry instanceof IOObjectEntry) { - OpenAction.showAsResult((IOObjectEntry) entry); + showAsResult((IOObjectEntry) entry); } else { throw new RepositoryException("Cannot open entries of type " + entry.getType() + "."); } @@ -183,4 +187,15 @@ public static void open(String openLocation, boolean showInfo) { SwingTools.showSimpleErrorMessage("while_loading", e, openLocation, e.getMessage()); } } + + /** + * ConnectionManagement Frontend : show a dialog + * + * @param connectionEntry + * the entry to be shown + * @since 9.3 + */ + public static void showConnectionInformationDialog(ConnectionEntry connectionEntry) { + EditConnectionAction.editConnection(connectionEntry, false); + } } diff --git a/src/main/java/com/rapidminer/gui/actions/UpgradeLicenseAction.java b/src/main/java/com/rapidminer/gui/actions/UpgradeLicenseAction.java index bdaa1cd31..a632cd890 100644 --- a/src/main/java/com/rapidminer/gui/actions/UpgradeLicenseAction.java +++ b/src/main/java/com/rapidminer/gui/actions/UpgradeLicenseAction.java @@ -26,6 +26,7 @@ import javax.swing.SwingWorker; import com.rapidminer.core.license.ProductLinkRegistry; +import com.rapidminer.gui.tools.MultiSwingWorker; import com.rapidminer.gui.tools.NotificationPopup; import com.rapidminer.gui.tools.ResourceAction; import com.rapidminer.tools.I18N; @@ -67,7 +68,7 @@ public UpgradeLicenseAction(String productId) { @Override public void loggedActionPerformed(final ActionEvent e) { - createUpgradeWorker().execute(); + createUpgradeWorker().start(); if (e != null && e.getSource() != null) { NotificationPopup popup = (NotificationPopup) SwingUtilities.getAncestorOfClass(NotificationPopup.class, (Component) e.getSource()); @@ -82,8 +83,8 @@ public void loggedActionPerformed(final ActionEvent e) { * * @return */ - private SwingWorker createUpgradeWorker() { - return new SwingWorker() { + private MultiSwingWorker createUpgradeWorker() { + return new MultiSwingWorker() { @Override protected Void doInBackground() throws Exception { diff --git a/src/main/java/com/rapidminer/gui/actions/export/ComponentPrinter.java b/src/main/java/com/rapidminer/gui/actions/export/ComponentPrinter.java index c77799fd8..0a875d8ae 100644 --- a/src/main/java/com/rapidminer/gui/actions/export/ComponentPrinter.java +++ b/src/main/java/com/rapidminer/gui/actions/export/ComponentPrinter.java @@ -18,13 +18,16 @@ */ package com.rapidminer.gui.actions.export; +import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; +import java.awt.Toolkit; import java.awt.geom.Rectangle2D; import java.awt.print.PageFormat; import java.awt.print.Pageable; import java.awt.print.Printable; +import java.util.Map; import com.rapidminer.core.license.ProductConstraintManager; import com.rapidminer.gui.license.LicenseTools; @@ -44,7 +47,7 @@ public class ComponentPrinter implements Printable, Pageable { private PageFormat pageFormat = PrintingTools.getPrinterJob().defaultPage(); - public static final Font TITLE_FONT = FontTools.getFont("LucidaSans", Font.PLAIN, 9); + public static final Font TITLE_FONT = FontTools.getFont(Font.DIALOG, Font.PLAIN, 9); /** The given components that should be printed. */ public ComponentPrinter(PrintableComponent... components) { @@ -61,8 +64,8 @@ public int print(Graphics g, PageFormat pageFormat, int pageIndex) { * * @param g * the graphics object - * @param pageFormat - * the page format + * @param x the x coordinate + * @param y the y coordinate * @param width * the downscaled width * @param height @@ -71,16 +74,24 @@ public int print(Graphics g, PageFormat pageFormat, int pageIndex) { * the page index that should be printed */ public int print(Graphics g, double x, double y, double width, double height, int pageIndex) { - if (pageIndex >= components.length) { + if (pageIndex >= components.length || !(g instanceof Graphics2D)) { return NO_SUCH_PAGE; } + // Make the header text less pixelated + Graphics2D g2d = (Graphics2D) g; + Map desktopHints = (Map) Toolkit.getDefaultToolkit().getDesktopProperty("awt.font.desktophints"); + if (desktopHints != null) { + g2d.setRenderingHints(desktopHints); + } + String title = components[pageIndex].getExportName(); if (title == null) { title = LicenseTools.translateProductName(ProductConstraintManager.INSTANCE.getActiveLicense()); } - Rectangle2D rect = TITLE_FONT.getStringBounds(title, ((Graphics2D) g).getFontRenderContext()); + Rectangle2D rect = TITLE_FONT.getStringBounds(title, g2d.getFontRenderContext()); g.setFont(TITLE_FONT); + g.setColor(Color.BLACK); int stringX = (int) (x + width / 2 - rect.getWidth() / 2); int stringY = (int) (y - rect.getY()); g.drawString(title, stringX, stringY); diff --git a/src/main/java/com/rapidminer/gui/dialog/EULADialog.java b/src/main/java/com/rapidminer/gui/dialog/EULADialog.java index cbbde60a3..69788992b 100644 --- a/src/main/java/com/rapidminer/gui/dialog/EULADialog.java +++ b/src/main/java/com/rapidminer/gui/dialog/EULADialog.java @@ -78,7 +78,7 @@ public class EULADialog extends ButtonDialog implements AdjustmentListener, Chan /** * Should be adjusted whenever the EULA is updated. */ - private static final String ACCEPT_PROPERTY = "rapidminer.eula.v6.accepted"; + private static final String ACCEPT_PROPERTY = "rapidminer.eula.v7.accepted"; private final JButton acceptButton; private final JCheckBox acceptCheckBox; diff --git a/src/main/java/com/rapidminer/gui/dialog/OperatorInfoScreen.java b/src/main/java/com/rapidminer/gui/dialog/OperatorInfoScreen.java index 8c2a1cc99..c9ee3df09 100644 --- a/src/main/java/com/rapidminer/gui/dialog/OperatorInfoScreen.java +++ b/src/main/java/com/rapidminer/gui/dialog/OperatorInfoScreen.java @@ -25,10 +25,11 @@ import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.Insets; +import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; -import java.awt.event.ComponentListener; import java.util.LinkedList; - +import java.util.Objects; +import java.util.Optional; import javax.swing.BorderFactory; import javax.swing.Icon; import javax.swing.JLabel; @@ -40,6 +41,7 @@ import com.rapidminer.gui.ApplicationFrame; import com.rapidminer.gui.OperatorDocumentationBrowser; +import com.rapidminer.gui.renderer.RendererService; import com.rapidminer.gui.tools.ExtendedJScrollPane; import com.rapidminer.gui.tools.ResourceLabel; import com.rapidminer.gui.tools.SwingTools; @@ -50,8 +52,13 @@ import com.rapidminer.operator.OperatorCapability; import com.rapidminer.operator.OperatorChain; import com.rapidminer.operator.learner.CapabilityProvider; +import com.rapidminer.operator.ports.InputPort; +import com.rapidminer.operator.ports.OutputPort; import com.rapidminer.operator.ports.Port; import com.rapidminer.operator.ports.Ports; +import com.rapidminer.operator.ports.metadata.MetaData; +import com.rapidminer.operator.ports.metadata.Precondition; +import com.rapidminer.tools.I18N; /** @@ -64,6 +71,7 @@ public class OperatorInfoScreen extends ButtonDialog { private static final long serialVersionUID = -6566133238783779634L; + public static final String INFO_SCREEN_PREFIX = "operator_info_screen."; private final transient Operator operator; @@ -91,7 +99,7 @@ public OperatorInfoScreen(Operator operator) { final JLabel label = new JLabel(SwingTools.createIcon("24/sign_warning.png")); label.setHorizontalTextPosition(SwingConstants.CENTER); label.setVerticalTextPosition(SwingConstants.BOTTOM); - label.setText("Depreceated!"); + label.setText("Deprecated!"); label.setPreferredSize(new Dimension(180, 50)); label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 40)); deprecatedPanel.add(label, BorderLayout.WEST); @@ -104,10 +112,10 @@ public OperatorInfoScreen(Operator operator) { if (operator instanceof CapabilityProvider) { JPanel learnerPanel = new JPanel(new BorderLayout()); - JLabel label = new JLabel(SwingTools.createIcon("24/briefcase2.png")); + JLabel label = new ResourceLabel(INFO_SCREEN_PREFIX + "capabilities"); + label.setHorizontalAlignment(SwingConstants.CENTER); label.setHorizontalTextPosition(SwingConstants.CENTER); label.setVerticalTextPosition(SwingConstants.BOTTOM); - label.setText("Capabilities"); label.setPreferredSize(new Dimension(180, 50)); label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 40)); learnerPanel.add(label, BorderLayout.WEST); @@ -123,16 +131,17 @@ public OperatorInfoScreen(Operator operator) { // ports if (operator.getInputPorts().getNumberOfPorts() > 0 || operator.getOutputPorts().getNumberOfPorts() > 0) { JPanel portPanel = new JPanel(new BorderLayout()); - JLabel label = new JLabel(SwingTools.createIcon("24/plug.png")); + JLabel label = new ResourceLabel(INFO_SCREEN_PREFIX + "ports"); + label.setHorizontalAlignment(SwingConstants.CENTER); label.setHorizontalTextPosition(SwingConstants.CENTER); label.setVerticalTextPosition(SwingConstants.BOTTOM); - label.setText("Ports"); label.setPreferredSize(new Dimension(180, 50)); label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 40)); portPanel.add(label, BorderLayout.WEST); portPanel.add( - createPortsDescriptionPanel("input_ports", "output_ports", operator.getInputPorts(), + createPortsDescriptionPanel(INFO_SCREEN_PREFIX + "input_ports", + INFO_SCREEN_PREFIX + "output_ports", operator.getInputPorts(), operator.getOutputPorts()), BorderLayout.CENTER); portPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createMatteBorder(0, 0, 1, 0, Color.LIGHT_GRAY), @@ -146,7 +155,8 @@ public OperatorInfoScreen(Operator operator) { for (ExecutionUnit subprocess : chain.getSubprocesses()) { JPanel subprocessPanel = new JPanel(new BorderLayout()); - JLabel label = new JLabel(SwingTools.createIcon("24/elements_tree.png")); + JLabel label = new ResourceLabel(INFO_SCREEN_PREFIX + "subprocess"); + label.setHorizontalAlignment(SwingConstants.CENTER); label.setHorizontalTextPosition(SwingConstants.CENTER); label.setVerticalTextPosition(SwingConstants.BOTTOM); label.setText(subprocess.getName()); @@ -155,7 +165,8 @@ public OperatorInfoScreen(Operator operator) { subprocessPanel.add(label, BorderLayout.WEST); subprocessPanel.add( - createPortsDescriptionPanel("inner_sources", "inner_sinks", subprocess.getInnerSources(), + createPortsDescriptionPanel(INFO_SCREEN_PREFIX + "inner_sources", + INFO_SCREEN_PREFIX + "inner_sinks", subprocess.getInnerSources(), subprocess.getInnerSinks()), BorderLayout.CENTER); subprocessPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createMatteBorder(0, 0, 1, 0, Color.LIGHT_GRAY), @@ -171,13 +182,7 @@ public OperatorInfoScreen(Operator operator) { final JScrollPane overviewPane = new ExtendedJScrollPane(overviewPanel); overviewPane.setBorder(null); overviewPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); - overviewPane.getViewport().addComponentListener(new ComponentListener() { - - @Override - public void componentHidden(ComponentEvent e) {} - - @Override - public void componentMoved(ComponentEvent e) {} + overviewPane.getViewport().addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { @@ -185,13 +190,9 @@ public void componentResized(ComponentEvent e) { overviewPanel.setPreferredSize(new Dimension((int) overviewPane.getViewport().getExtentSize().getWidth(), (int) overviewPanel.getPreferredSize().getHeight())); } - - @Override - public void componentShown(ComponentEvent e) {} - }); - tabs.add("Overview", overviewPane); - tabs.add("Description", browser); + tabs.add(I18N.getGUILabel(INFO_SCREEN_PREFIX + "tab.overview.label"), overviewPane); + tabs.add(I18N.getGUILabel(INFO_SCREEN_PREFIX + "tab.description.label"), browser); layoutDefault(tabs, NORMAL, makeCloseButton()); } @@ -205,7 +206,7 @@ protected String getInfoText() { return "" + operator.getOperatorDescription().getName() + "" - + (operator.getOperatorDescription().getGroup().equals("") ? "" : "
Group: " + + (operator.getOperatorDescription().getGroup().isEmpty() ? "" : "
Group: " + operator.getOperatorDescription().getGroupName()) + ""; } @@ -230,42 +231,30 @@ public static JPanel createPortsDescriptionPanel(String inKey, String outKey, Po c.weighty = 1; final JPanel panel = new JPanel(layout); - final Icon inIcon; - final Icon outIcon; { JPanel rowPanel = new JPanel(new GridLayout(1, 2)); JLabel label = new ResourceLabel(inKey); label.setText("" + label.getText() + ""); - inIcon = label.getIcon(); label.setIcon(null); rowPanel.add(label); label = new ResourceLabel(outKey); label.setText("" + label.getText() + ""); - outIcon = label.getIcon(); label.setIcon(null); rowPanel.add(label); panel.add(rowPanel, c); } - final LinkedList labels = new LinkedList(); + final LinkedList labels = new LinkedList<>(); for (int i = 0; i < Math.max(numberOfInputPorts, numberOfOutputPorts); i++) { JPanel rowPanel = new JPanel(new GridLayout(1, 2)); if (i < numberOfInputPorts) { - Port port = inputPorts.getPortByIndex(i); - FixedWidthLabel label = new FixedWidthLabel(rowPanel.getWidth() / 2, port.getName()); - label.setIcon(inIcon); - label.setText(// (numberOfInputPorts > 1 ? ("" + (i + 1) + ": ") : "") + - port.getName() + (port.getDescription().equals("") ? "" : " (" + port.getDescription() + ")")); + FixedWidthLabel label = createPortLabel(inputPorts.getPortByIndex(i), rowPanel.getWidth()); labels.add(label); rowPanel.add(label); } else { rowPanel.add(new JLabel()); } if (i < numberOfOutputPorts) { - Port port = outputPorts.getPortByIndex(i); - FixedWidthLabel label = new FixedWidthLabel(rowPanel.getWidth() / 2, port.getName()); - label.setIcon(outIcon); - label.setText(// (numberOfOutputPorts > 1 ? ("" + (i + 1) + ": ") : "") + - port.getName() + (port.getDescription().equals("") ? "" : " (" + port.getDescription() + ")")); + FixedWidthLabel label = createPortLabel(outputPorts.getPortByIndex(i), rowPanel.getWidth()); labels.add(label); rowPanel.add(label); } else { @@ -273,13 +262,7 @@ public static JPanel createPortsDescriptionPanel(String inKey, String outKey, Po } panel.add(rowPanel, c); } - panel.addComponentListener(new ComponentListener() { - - @Override - public void componentHidden(ComponentEvent e) {} - - @Override - public void componentMoved(ComponentEvent e) {} + panel.addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { @@ -288,9 +271,6 @@ public void componentResized(ComponentEvent e) { label.setWidth((int) (panel.getWidth() / 2.2)); } } - - @Override - public void componentShown(ComponentEvent e) {} }); return panel; } @@ -299,21 +279,12 @@ public static JPanel createDeprecationInfoPanel(Operator operator) { final JPanel panel = new JPanel(new BorderLayout()); final FixedWidthLabel info = new FixedWidthLabel(200, operator.getOperatorDescription().getDeprecationInfo()); panel.add(info, BorderLayout.CENTER); - panel.addComponentListener(new ComponentListener() { - - @Override - public void componentHidden(ComponentEvent e) {} - - @Override - public void componentMoved(ComponentEvent e) {} + panel.addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { info.setWidth(panel.getWidth()); } - - @Override - public void componentShown(ComponentEvent e) {} }); return panel; } @@ -341,4 +312,36 @@ public static JPanel createCapabilitiesPanel(Operator operator) { return capabilitiesPanel; } + /** + * Creates a {@link FixedWidthLabel} for the given port, setting the given icon and putting the description + * of the port as text. + * + * @param port + * the port + * @param totalWidth + * the total width; label width will be half + * @return the label + * @since 9.3 + */ + private static FixedWidthLabel createPortLabel(Port port, int totalWidth) { + FixedWidthLabel label = new FixedWidthLabel(totalWidth / 2, port.getName()); + Icon portIcon = null; + if (port instanceof OutputPort) { + portIcon = RendererService.getIcon(Optional.of(port).map(Port::getMetaData) + .map(MetaData::getObjectClass).orElse(null)); + } else if (port instanceof InputPort) { + portIcon = RendererService.getIcon(((InputPort) port).getAllPreconditions().stream() + .map(Precondition::getExpectedMetaData) + .filter(Objects::nonNull).map(MetaData::getObjectClass) + .findFirst().orElse(null)); + } + label.setIcon(portIcon); + String portDesc = port.getDescription(); + if (!portDesc.isEmpty()) { + portDesc = " (" + portDesc + ')'; + } + label.setText(port.getName() + portDesc); + return label; + } + } diff --git a/src/main/java/com/rapidminer/gui/dnd/ReceivingOperatorTransferHandler.java b/src/main/java/com/rapidminer/gui/dnd/ReceivingOperatorTransferHandler.java index 0e063076b..a778fab70 100644 --- a/src/main/java/com/rapidminer/gui/dnd/ReceivingOperatorTransferHandler.java +++ b/src/main/java/com/rapidminer/gui/dnd/ReceivingOperatorTransferHandler.java @@ -26,9 +26,11 @@ import java.io.IOException; import java.io.StringReader; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.function.BooleanSupplier; import java.util.logging.Level; @@ -51,6 +53,7 @@ import com.rapidminer.io.process.XMLImporter; import com.rapidminer.io.process.XMLTools; import com.rapidminer.operator.Operator; +import com.rapidminer.operator.OperatorChain; import com.rapidminer.operator.OperatorCreationException; import com.rapidminer.operator.internal.ProcessEmbeddingOperator; import com.rapidminer.operator.io.RepositorySource; @@ -65,6 +68,7 @@ import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; import com.rapidminer.tools.OperatorService; +import com.rapidminer.tools.ProcessTools; import com.rapidminer.tools.Tools; import com.rapidminer.tools.usagestats.UsageLoggable; @@ -322,6 +326,15 @@ public boolean importData(TransferSupport ts) { dropNow = () -> dropNow((WorkflowAnnotation) transferData, null); } else { List droppedOperators = Tools.cloneOperators(newOperators); + + // find all operators that will be renamed and notify the cloned operators about that + Collection allOperatorNames = getProcess().getAllOperatorNames(); + List oldNames = findAllNames(newOperators); + Map nameMap = ProcessTools.getNewNames(allOperatorNames, oldNames); + if (!nameMap.isEmpty()) { + droppedOperators.forEach(op -> nameMap.forEach(op::notifyRenaming)); + } + dropNow = () -> dropNow(droppedOperators, null); } boolean result = false; @@ -345,6 +358,17 @@ public boolean importData(TransferSupport ts) { } } + /** + * Finds the names of all operators, including inner operators + * + * @since 9.3 + */ + private List findAllNames(List operators) { + return operators.stream().flatMap(op-> op instanceof OperatorChain ? + ((OperatorChain) op).getAllInnerOperatorsAndMe().stream().map(Operator::getName) + : Stream.of(op.getName())).collect(Collectors.toList()); + } + /** * Creates the operator to import from the given repository location. * diff --git a/src/main/java/com/rapidminer/gui/flow/processrendering/view/ProcessRendererView.java b/src/main/java/com/rapidminer/gui/flow/processrendering/view/ProcessRendererView.java index ba9bd22a0..acb8396f2 100644 --- a/src/main/java/com/rapidminer/gui/flow/processrendering/view/ProcessRendererView.java +++ b/src/main/java/com/rapidminer/gui/flow/processrendering/view/ProcessRendererView.java @@ -41,6 +41,7 @@ import java.util.Collection; import java.util.Collections; import java.util.EnumMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; @@ -79,6 +80,7 @@ import com.rapidminer.gui.flow.PanningManager; import com.rapidminer.gui.flow.ProcessInteractionListener; import com.rapidminer.gui.flow.ProcessPanel; +import com.rapidminer.gui.flow.processrendering.annotations.model.OperatorAnnotation; import com.rapidminer.gui.flow.processrendering.annotations.model.WorkflowAnnotation; import com.rapidminer.gui.flow.processrendering.annotations.model.WorkflowAnnotations; import com.rapidminer.gui.flow.processrendering.draw.OperatorDrawDecorator; @@ -803,6 +805,7 @@ public void operatorsChanged(ProcessRendererOperatorEvent e, Collection opsToMove = new LinkedHashSet<>(operators); boolean wasResized = false; boolean shouldMoveOperators = Boolean.parseBoolean(ParameterService.getParameterValue(RapidMinerGUI.PROPERTY_RAPIDMINER_GUI_MOVE_CONNECTED_OPERATORS)); + Set movedAnnotations = new HashSet<>(); while (!opsToMove.isEmpty()) { Iterator iterator = opsToMove.iterator(); Operator op = iterator.next(); @@ -817,25 +820,42 @@ public void operatorsChanged(ProcessRendererOperatorEvent e, Collection r != null && Math.abs(r.getY() - operatorRect.getY()) < r.getHeight()) + .filter(r -> r != null && isOverlapping(r, operatorRect)) .mapToDouble(r -> r.getX() + ProcessDrawer.GRID_AUTOARRANGE_WIDTH - 1) - .filter(x -> operatorRect.getX() < x) - .forEach(x -> { + .filter(x -> operatorRect.getX() < x).max() + .ifPresent(x -> { + double diff = operatorRect.getX() - x; operatorRect.setRect(x, operatorRect.getY(), operatorRect.getWidth(), operatorRect.getHeight()); model.setOperatorRect(op, operatorRect); + final WorkflowAnnotations operatorAnnotations = model.getOperatorAnnotations(op); + if (operatorAnnotations != null && !operatorAnnotations.isEmpty()) { + List annotationsEventOrder = operatorAnnotations.getAnnotationsEventOrder(); + annotationsEventOrder.stream().filter(anno -> anno instanceof OperatorAnnotation) + .map(WorkflowAnnotation::getLocation) + .forEach(r -> r.setRect(r.getX() - diff, r.getY(), r.getWidth(), r.getHeight())); + movedAnnotations.addAll(annotationsEventOrder); + } }); // check all connected operators to the right also - opsToMove.addAll(rightConnectedOperators); + // only if this operator really moved either manually or by this feature + if (!oldOperatorRect.equals(operatorRect) || operators.contains(op)) { + opsToMove.addAll(rightConnectedOperators); + } } wasResized |= controller.ensureProcessSizeFits(executionUnit, model.getOperatorRect(op)); // notify registered listeners fireOperatorMoved(op); } + if (!movedAnnotations.isEmpty()) { + model.fireAnnotationsMoved(movedAnnotations); + } + // need to repaint if process was not resized if (!wasResized) { repaint(); @@ -860,6 +880,16 @@ private

List getDirectlyConnectedPorts(Ports

ports, .filter(co -> executionUnit == co.getExecutionUnit() && !exclude.contains(co)).distinct().collect(Collectors.toList()); } + /** @since 9.2.1 */ + private boolean isOverlapping(Rectangle2D a, Rectangle2D b) { + return isOverlappingTop(a, b) || isOverlappingTop(b, a); + } + + /** @since 9.2.1 */ + private boolean isOverlappingTop(Rectangle2D top, Rectangle2D bottom) { + return top.getY() <= bottom.getY() && bottom.getY() <= top.getMaxY(); + } + @Override public void annotationsChanged(ProcessRendererAnnotationEvent e, Collection annotations) { switch (e.getEventType()) { diff --git a/src/main/java/com/rapidminer/gui/graphs/SingleDefaultGraphMouse.java b/src/main/java/com/rapidminer/gui/graphs/SingleDefaultGraphMouse.java index 5c6623c29..f64eec359 100644 --- a/src/main/java/com/rapidminer/gui/graphs/SingleDefaultGraphMouse.java +++ b/src/main/java/com/rapidminer/gui/graphs/SingleDefaultGraphMouse.java @@ -1,5 +1,20 @@ /** - * Copyright (C) 2001-2019 RapidMiner GmbH + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.gui.graphs; diff --git a/src/main/java/com/rapidminer/gui/graphs/plugins/ExtendedPickingGraphMousePlugin.java b/src/main/java/com/rapidminer/gui/graphs/plugins/ExtendedPickingGraphMousePlugin.java index ad6f343b5..49de9a555 100644 --- a/src/main/java/com/rapidminer/gui/graphs/plugins/ExtendedPickingGraphMousePlugin.java +++ b/src/main/java/com/rapidminer/gui/graphs/plugins/ExtendedPickingGraphMousePlugin.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ package com.rapidminer.gui.graphs.plugins; import java.awt.Point; diff --git a/src/main/java/com/rapidminer/gui/look/RapidLookAndFeel.java b/src/main/java/com/rapidminer/gui/look/RapidLookAndFeel.java index 9cb84c530..8d6fc0358 100644 --- a/src/main/java/com/rapidminer/gui/look/RapidLookAndFeel.java +++ b/src/main/java/com/rapidminer/gui/look/RapidLookAndFeel.java @@ -20,6 +20,7 @@ import java.awt.Color; import java.awt.Font; +import java.util.Collections; import java.util.HashMap; import java.util.Hashtable; import java.util.Map; @@ -117,7 +118,7 @@ public UIDefaults getDefaults() { // enables AntiAliasing if AntiAliasing is enabled in the OS // EXCEPT for key "Menu.opaque" which will glitch out JMenues UIDefaults lookAndFeelDefaults = UIManager.getLookAndFeelDefaults(); - Hashtable copy = new Hashtable<>(lookAndFeelDefaults); + Hashtable copy = new Hashtable<>(lookAndFeelDefaults != null ? lookAndFeelDefaults : Collections.emptyMap()); for (Object key : copy.keySet()) { if (!String.valueOf(key).equals("Menu.opaque")) { table.put(key, lookAndFeelDefaults.get(key)); diff --git a/src/main/java/com/rapidminer/gui/look/RapidLookComboBoxEditor.java b/src/main/java/com/rapidminer/gui/look/RapidLookComboBoxEditor.java index aa814f89b..0fafe0ca6 100644 --- a/src/main/java/com/rapidminer/gui/look/RapidLookComboBoxEditor.java +++ b/src/main/java/com/rapidminer/gui/look/RapidLookComboBoxEditor.java @@ -18,15 +18,10 @@ */ package com.rapidminer.gui.look; -import java.awt.Color; -import java.awt.Font; - import javax.swing.JTextField; import javax.swing.plaf.basic.BasicComboBoxEditor; -import org.jdesktop.swingx.prompt.PromptSupport; -import org.jdesktop.swingx.prompt.PromptSupport.FocusBehavior; - +import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.tools.I18N; @@ -51,12 +46,9 @@ public void putClientProperty(Object key, Object val) { public void setEnable(boolean val) { this.editor.setEnabled(val); if (val) { - PromptSupport.setForeground(Color.LIGHT_GRAY, textField); - PromptSupport.setPrompt(PROMPT, textField); - PromptSupport.setFontStyle(Font.ITALIC, textField); - PromptSupport.setFocusBehavior(FocusBehavior.SHOW_PROMPT, textField); + SwingTools.setPrompt(PROMPT, textField); } else { - PromptSupport.setPrompt("", textField); + SwingTools.setPrompt("", textField); } } diff --git a/src/main/java/com/rapidminer/gui/look/fc/MultipleLinesLabel.java b/src/main/java/com/rapidminer/gui/look/fc/MultipleLinesLabel.java index 2fe0ad9ef..650ef9cde 100644 --- a/src/main/java/com/rapidminer/gui/look/fc/MultipleLinesLabel.java +++ b/src/main/java/com/rapidminer/gui/look/fc/MultipleLinesLabel.java @@ -63,7 +63,7 @@ public class MultipleLinesLabel extends JComponent implements SwingConstants { private FontMetrics fontMetrics; - private Vector vector; + private Vector vector = new Vector<>(); private boolean needUpdate = true; diff --git a/src/main/java/com/rapidminer/gui/look/ui/MultiStepProgressBar.java b/src/main/java/com/rapidminer/gui/look/ui/MultiStepProgressBar.java index 706dda2c1..1eaf5a146 100644 --- a/src/main/java/com/rapidminer/gui/look/ui/MultiStepProgressBar.java +++ b/src/main/java/com/rapidminer/gui/look/ui/MultiStepProgressBar.java @@ -1,5 +1,20 @@ /** - * Copyright (C) 2001-2019 RapidMiner GmbH + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.gui.look.ui; diff --git a/src/main/java/com/rapidminer/gui/look/ui/TabbedPaneUI.java b/src/main/java/com/rapidminer/gui/look/ui/TabbedPaneUI.java index d68fa63e3..09a9ff00e 100644 --- a/src/main/java/com/rapidminer/gui/look/ui/TabbedPaneUI.java +++ b/src/main/java/com/rapidminer/gui/look/ui/TabbedPaneUI.java @@ -36,7 +36,6 @@ import java.awt.geom.Path2D; import java.awt.geom.QuadCurve2D; import java.beans.PropertyChangeListener; - import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JTabbedPane; @@ -163,7 +162,35 @@ protected FontMetrics getFontMetrics() { @Override protected MouseListener createMouseListener() { - return new MouseHandler(); + MouseListener defaultMouseListener = super.createMouseListener(); + return new MouseListener() { + + public void mouseEntered(MouseEvent e) { + defaultMouseListener.mouseEntered(e); + } + + public void mouseExited(MouseEvent e) { + defaultMouseListener.mouseExited(e); + } + + @Override + public void mouseClicked(MouseEvent e) { + defaultMouseListener.mouseClicked(e); + } + + public void mousePressed(MouseEvent e) { + // by default, Java changes tabs with ANY mouse button (left, right, middle, thumb buttons, ...) + // we only want a tab change on left click. Additional actions need to be registered by developer anyhow + if (SwingUtilities.isLeftMouseButton(e)) { + defaultMouseListener.mousePressed(e); + } + } + + @Override + public void mouseReleased(MouseEvent e) { + defaultMouseListener.mouseReleased(e); + } + }; } @Override diff --git a/src/main/java/com/rapidminer/gui/new_plotter/engine/jfreechart/JFreeChartPlotEngine.java b/src/main/java/com/rapidminer/gui/new_plotter/engine/jfreechart/JFreeChartPlotEngine.java index bf8e89cfc..4b45cce9b 100644 --- a/src/main/java/com/rapidminer/gui/new_plotter/engine/jfreechart/JFreeChartPlotEngine.java +++ b/src/main/java/com/rapidminer/gui/new_plotter/engine/jfreechart/JFreeChartPlotEngine.java @@ -116,6 +116,7 @@ import com.rapidminer.gui.new_plotter.listener.events.ValueSourceChangeEvent.ValueSourceChangeType; import com.rapidminer.gui.plotter.CoordinateTransformation; import com.rapidminer.gui.plotter.NullCoordinateTransformation; +import com.rapidminer.gui.tools.MultiSwingWorker; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.tools.FontTools; import com.rapidminer.tools.I18N; @@ -309,7 +310,7 @@ public JFreeChart getCurrentChart() { private synchronized void updateChartPanelChart(final boolean informPlotConfigWhenDone) { updatingChart.getAndSet(true); - SwingWorker updateChartWorker = new SwingWorker() { + MultiSwingWorker updateChartWorker = new MultiSwingWorker() { @Override public JFreeChart doInBackground() throws Exception { @@ -368,7 +369,7 @@ public void done() { }; - updateChartWorker.execute(); + updateChartWorker.start(); } /** diff --git a/src/main/java/com/rapidminer/gui/operatormenu/ReplaceOperatorMenu.java b/src/main/java/com/rapidminer/gui/operatormenu/ReplaceOperatorMenu.java index 71aa17c40..1d258bbbd 100644 --- a/src/main/java/com/rapidminer/gui/operatormenu/ReplaceOperatorMenu.java +++ b/src/main/java/com/rapidminer/gui/operatormenu/ReplaceOperatorMenu.java @@ -19,10 +19,14 @@ package com.rapidminer.gui.operatormenu; import java.awt.geom.Rectangle2D; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Function; +import java.util.stream.Collectors; +import com.rapidminer.Process; import com.rapidminer.gui.MainFrame; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawUtils; @@ -34,7 +38,13 @@ import com.rapidminer.operator.OperatorDescription; import com.rapidminer.operator.ports.InputPort; import com.rapidminer.operator.ports.OutputPort; +import com.rapidminer.operator.ports.Port; +import com.rapidminer.operator.ports.Ports; +import com.rapidminer.parameter.ParameterType; +import com.rapidminer.parameter.Parameters; import com.rapidminer.tools.OperatorService; +import com.rapidminer.tools.ProcessTools; +import com.rapidminer.tools.container.Pair; /** @@ -77,24 +87,19 @@ private void replace(Operator operator) { } // remember source and sink connections so we can reconnect them later. - Map inputPortMap = new HashMap<>(); - Map outputPortMap = new HashMap<>(); - for (OutputPort source : selectedOperator.getOutputPorts().getAllPorts()) { - if (source.isConnected()) { - inputPortMap.put(source.getName(), source.getDestination()); - source.lock(); - source.getDestination().lock(); + Map inputPortMap = getConnectedPorts(selectedOperator.getOutputPorts(), OutputPort::getDestination); + Map outputPortMap = getConnectedPorts(selectedOperator.getInputPorts(), InputPort::getSource); + + // copy parameters if possible + Parameters oldParameters = selectedOperator.getParameters(); + Parameters newParameters = operator.getParameters(); + for (String key : oldParameters.getDefinedKeys()) { + ParameterType newType = newParameters.getParameterType(key); + // copy if parameter types match + if (newType != null && oldParameters.getParameterType(key).getClass() == newType.getClass()) { + newParameters.setParameter(key, oldParameters.getParameterOrNull(key)); } } - for (InputPort sink : selectedOperator.getInputPorts().getAllPorts()) { - if (sink.isConnected()) { - outputPortMap.put(sink.getName(), sink.getSource()); - sink.lock(); - sink.getSource().lock(); - } - } - selectedOperator.getOutputPorts().disconnectAll(); - selectedOperator.getInputPorts().disconnectAll(); int failedReconnects = 0; @@ -104,37 +109,25 @@ private void replace(Operator operator) { OperatorChain newChain = (OperatorChain) operator; int numCommonSubprocesses = Math.min(oldChain.getNumberOfSubprocesses(), newChain.getNumberOfSubprocesses()); for (int i = 0; i < numCommonSubprocesses; i++) { - ExecutionUnit oldSubprocess = oldChain.getSubprocess(i); - ExecutionUnit newSubprocess = newChain.getSubprocess(i); - failedReconnects += newSubprocess.stealOperatorsFrom(oldSubprocess); + failedReconnects += newChain.getSubprocess(i).stealOperatorsFrom(oldChain.getSubprocess(i)); } } int oldPos = parent.getOperators().indexOf(selectedOperator); + Process process = selectedOperator.getProcess(); selectedOperator.remove(); - parent.addOperator(operator, oldPos); - // Rewire sources and sinks - for (Map.Entry entry : inputPortMap.entrySet()) { - OutputPort mySource = operator.getOutputPorts().getPortByName(entry.getKey()); - if (mySource != null) { - mySource.connectTo(entry.getValue()); - mySource.unlock(); - entry.getValue().unlock(); - } else { - failedReconnects++; - } + if (process != null) { + // find actual new name within process + String newName = ProcessTools.getNewName(process.getAllOperatorNames(), operator.getName()); + // inform parameters of update + process.notifyReplacing(selectedOperator.getName(), selectedOperator, newName, operator); } - for (Map.Entry entry : outputPortMap.entrySet()) { - InputPort mySink = operator.getInputPorts().getPortByName(entry.getKey()); - if (mySink != null) { - entry.getValue().connectTo(mySink); - entry.getValue().unlock(); - mySink.unlock(); - } else { - failedReconnects++; - } - } + parent.addOperator(operator, oldPos); + + // Rewire sources and sinks + failedReconnects += rewirePorts(inputPortMap, operator.getOutputPorts()); + failedReconnects += rewirePorts(outputPortMap, operator.getInputPorts()); // copy operator rectangle from old operator to the new one to make the swap in place ProcessRendererModel processModel = mainFrame.getProcessPanel().getProcessRenderer().getModel(); @@ -147,6 +140,62 @@ private void replace(Operator operator) { if (failedReconnects > 0) { SwingTools.showVerySimpleErrorMessage("op_replaced_failed_connections_restored", failedReconnects); } + } + + /** + * Gets a map of all connected ports to its connected counter part. The key is the name of the port found by the + * given {@link Ports} instance, the value is the actual {@link Port} object connected to it. The port name corresponds + * to a port belonging to the operator to be replaced. + * + * @since 9.3 + */ + private

LinkedHashMap getConnectedPorts(Ports

ports, Function opposite) { + return ports.getAllPorts().stream().filter(Port::isConnected).map(p -> getDisconnectedLockedPair(p, opposite.apply(p))) + .collect(Collectors.toMap(Pair::getFirst, Pair::getSecond, (a, b) -> b, LinkedHashMap::new)); + } + /** + * Gets a pair of connected {@link Port Ports}. Locks both ports, disconnects them and returns the a {@link Pair} + * with the first port's name and the second port. + * + * @see Port#lock() + * @since 9.3 + */ + private Pair getDisconnectedLockedPair(Port port, Port other) { + port.lock(); + other.lock(); + if (port instanceof OutputPort) { + ((OutputPort) port).disconnect(); + } else if (other instanceof OutputPort) { + ((OutputPort) other).disconnect(); + } + return new Pair<>(port.getName(), other); + } + + /** + * Connects all port pairs specified in the map using the {@link Ports port finder} to resolve ports by name. Returns the number + * of failed connections. + * + * @see Ports#getPortByName(String) + * @since 9.3 + */ + private int rewirePorts(Map connectedPorts, Ports ports) { + int sum = 0; + for (Entry e : connectedPorts.entrySet()) { + Port p = ports.getPortByName(e.getKey()); + if (p == null) { + sum++; + continue; + } + Port q = e.getValue(); + if (p instanceof OutputPort) { + ((OutputPort) p).connectTo((InputPort) q); + } else { + ((OutputPort) q).connectTo((InputPort) p); + } + p.unlock(); + q.unlock(); + } + return sum; } } diff --git a/src/main/java/com/rapidminer/gui/operatortree/actions/CutCopyPasteDeleteAction.java b/src/main/java/com/rapidminer/gui/operatortree/actions/CutCopyPasteDeleteAction.java index 16f48391e..8d1a4022e 100644 --- a/src/main/java/com/rapidminer/gui/operatortree/actions/CutCopyPasteDeleteAction.java +++ b/src/main/java/com/rapidminer/gui/operatortree/actions/CutCopyPasteDeleteAction.java @@ -50,16 +50,12 @@ private CutCopyPasteDeleteAction(String i18nKey, String action) { super(i18nKey); putValue(ACTION_COMMAND_KEY, action); KeyboardFocusManager manager = KeyboardFocusManager.getCurrentKeyboardFocusManager(); - manager.addPropertyChangeListener("permanentFocusOwner", new PropertyChangeListener() { - - @Override - public void propertyChange(PropertyChangeEvent evt) { - Object o = evt.getNewValue(); - if (o instanceof JComponent) { - focusOwner = (JComponent) o; - } else { - focusOwner = null; - } + manager.addPropertyChangeListener("permanentFocusOwner", evt -> { + Object o = evt.getNewValue(); + if (o instanceof JComponent) { + focusOwner = (JComponent) o; + } else { + focusOwner = null; } }); } diff --git a/src/main/java/com/rapidminer/gui/popup/PopupPanel.java b/src/main/java/com/rapidminer/gui/popup/PopupPanel.java index ce9a71d63..38bf90d7d 100644 --- a/src/main/java/com/rapidminer/gui/popup/PopupPanel.java +++ b/src/main/java/com/rapidminer/gui/popup/PopupPanel.java @@ -45,7 +45,7 @@ public class PopupPanel extends JPanel implements PropertyChangeListener { private static final long serialVersionUID = 1L; - private final List listenerList = new LinkedList<>(); + private final List listeners = new LinkedList<>(); private static final String PERMANENT_FOCUS_OWNER = "permanentFocusOwner"; @@ -106,11 +106,11 @@ public boolean requestFocusInWindow() { } public void addListener(PopupComponentListener l) { - listenerList.add(l); + listeners.add(l); } public void removeListener(PopupComponentListener l) { - listenerList.remove(l); + listeners.remove(l); } /** @@ -130,7 +130,7 @@ void stopTracking() { } private void fireFocusLost() { - for (PopupComponentListener l : listenerList) { + for (PopupComponentListener l : listeners) { l.focusLost(); } stopTracking(); diff --git a/src/main/java/com/rapidminer/gui/processeditor/MacroEditor.java b/src/main/java/com/rapidminer/gui/processeditor/MacroEditor.java index 44c4ea913..03a65f63c 100644 --- a/src/main/java/com/rapidminer/gui/processeditor/MacroEditor.java +++ b/src/main/java/com/rapidminer/gui/processeditor/MacroEditor.java @@ -180,8 +180,13 @@ public void loggedActionPerformed(ActionEvent e) { } private void addMacro() { + int previousMacroCount = context.getMacros().size(); context.addMacro(new Pair<>("", "")); - macroModel.fireAdd(); + + // if an empty macro already existed, nothing will change, so we don't want to fire an update here + if (context.getMacros().size() > previousMacroCount) { + macroModel.fireAdd(); + } } private void removeMacros() { diff --git a/src/main/java/com/rapidminer/gui/processeditor/results/DockableResultDisplay.java b/src/main/java/com/rapidminer/gui/processeditor/results/DockableResultDisplay.java index 5da6b8d43..a0ae8579b 100644 --- a/src/main/java/com/rapidminer/gui/processeditor/results/DockableResultDisplay.java +++ b/src/main/java/com/rapidminer/gui/processeditor/results/DockableResultDisplay.java @@ -374,9 +374,20 @@ private void clearNow(boolean alsoClearLogs) { @Override public void clearAll() { + clearAllExcept(); + } + + @Override + public void clearAllExcept(String... key) { + if (key == null) { + return; + } + for (DockableState state : RapidMinerGUI.getMainFrame().getDockingDesktop().getContext().getDockables()) { - if (state.getDockable().getDockKey().getKey().startsWith(ResultTab.DOCKKEY_PREFIX) - || state.getDockable().getDockKey().getKey().startsWith(ProcessLogTab.DOCKKEY_PREFIX)) { + List exemptionKeys = Arrays.asList(key); + String dockKeyKey = state.getDockable().getDockKey().getKey(); + if ((dockKeyKey.startsWith(ResultTab.DOCKKEY_PREFIX) || dockKeyKey.startsWith(ProcessLogTab.DOCKKEY_PREFIX)) && !exemptionKeys.contains(dockKeyKey)) { + // skipped if dock key is in exemption list RapidMinerGUI.getMainFrame().getPerspectiveController().removeFromAllPerspectives(state.getDockable()); } } diff --git a/src/main/java/com/rapidminer/gui/processeditor/results/ResultDisplay.java b/src/main/java/com/rapidminer/gui/processeditor/results/ResultDisplay.java index 54ce2562b..969950c04 100644 --- a/src/main/java/com/rapidminer/gui/processeditor/results/ResultDisplay.java +++ b/src/main/java/com/rapidminer/gui/processeditor/results/ResultDisplay.java @@ -34,17 +34,29 @@ */ public interface ResultDisplay extends Dockable, ProcessEditor { - public static final String RESULT_DOCK_KEY = "result"; + String RESULT_DOCK_KEY = "result"; /** Initializer called after the main frame is set up. */ - public void init(MainFrame mainFrame); + void init(MainFrame mainFrame); - public void showResult(ResultObject result); + void showResult(ResultObject result); - public void showData(final IOContainer resultContainer, final String message); + void showData(final IOContainer resultContainer, final String message); - public void addDataTable(DataTable dataTable); + void addDataTable(DataTable dataTable); - public void clearAll(); + void clearAll(); + + /** + * Removes all results from this result display except the results with the given key(s). + * + * @param key + * optional keys of results that should not be removed. If no keys are given, behaves the same as {@link + * #clearAll()} + * @since 9.3 + */ + default void clearAllExcept(String... key) { + clearAll(); + } } diff --git a/src/main/java/com/rapidminer/gui/processeditor/results/ResultTab.java b/src/main/java/com/rapidminer/gui/processeditor/results/ResultTab.java index 26704e05c..568f106ba 100644 --- a/src/main/java/com/rapidminer/gui/processeditor/results/ResultTab.java +++ b/src/main/java/com/rapidminer/gui/processeditor/results/ResultTab.java @@ -36,6 +36,7 @@ import com.rapidminer.gui.MainFrame; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.actions.CloseAllResultsAction; +import com.rapidminer.gui.actions.CloseAllResultsExceptCurrentResultAction; import com.rapidminer.gui.actions.StoreInRepositoryAction; import com.rapidminer.gui.renderer.RendererService; import com.rapidminer.gui.tools.ProgressThread; @@ -92,8 +93,10 @@ public ResultTab(String id) { @Override public void visitTabSelectorPopUp(JPopupMenu popUpMenu, Dockable dockable) { - popUpMenu.add(new JMenuItem(new StoreInRepositoryAction(resultObject))); + popUpMenu.add(new JMenuItem(new CloseAllResultsExceptCurrentResultAction(RapidMinerGUI.getMainFrame(), dockKey.getKey()))); popUpMenu.add(new JMenuItem(new CloseAllResultsAction(RapidMinerGUI.getMainFrame()))); + popUpMenu.addSeparator(); + popUpMenu.add(new JMenuItem(new StoreInRepositoryAction(resultObject))); } }; customizer.setTabSelectorPopUpCustomizer(true); // enable tabbed dock custom popup menu diff --git a/src/main/java/com/rapidminer/gui/processeditor/results/SingleResultOverview.java b/src/main/java/com/rapidminer/gui/processeditor/results/SingleResultOverview.java index 8ba6d7783..027550dd8 100644 --- a/src/main/java/com/rapidminer/gui/processeditor/results/SingleResultOverview.java +++ b/src/main/java/com/rapidminer/gui/processeditor/results/SingleResultOverview.java @@ -53,6 +53,7 @@ import com.rapidminer.gui.renderer.Renderer; import com.rapidminer.gui.renderer.RendererService; import com.rapidminer.gui.tools.ExtendedHTMLJEditorPane; +import com.rapidminer.gui.tools.MultiSwingWorker; import com.rapidminer.gui.tools.ProgressThread; import com.rapidminer.gui.tools.ResourceAction; import com.rapidminer.gui.tools.SwingTools; @@ -67,6 +68,7 @@ import com.rapidminer.repository.IOObjectEntry; import com.rapidminer.repository.RepositoryLocation; import com.rapidminer.tools.ReferenceCache; +import com.rapidminer.tools.Tools; /** @@ -330,11 +332,11 @@ private Component makeMainLabel(String text) { css.addRule("h4 {margin-bottom:0; margin-top:1ex; padding:0}"); css.addRule("p {margin-top:0; margin-bottom:1ex; padding:0}"); css.addRule("ul {margin-top:0; margin-bottom:1ex; list-style-image: url(" - + getClass().getResource("/com/rapidminer/resources/icons/modern/help/circle.png") + ")}"); + + Tools.getResource("icons/help/circle.png") + ")}"); css.addRule("ul li {padding-bottom: 2px}"); css.addRule("li.outPorts {padding-bottom: 0px}"); css.addRule("ul li ul {margin-top:0; margin-bottom:1ex; list-style-image: url(" - + getClass().getResource("/com/rapidminer/resources/icons/modern/help/line.png") + ")"); + + Tools.getResource("icons/help/line.png") + ")"); css.addRule("li ul li {padding-bottom:0}"); label.setEditable(false); @@ -350,7 +352,7 @@ private Component makeMainLabel(String text) { * Updates the preview renderable image in a {@link SwingWorker}. */ private void updatePreviewImage() { - final IOObject result = ioObject.get(); + final IOObject result = ioObject != null ? ioObject.get() : null; if (result != null) { String name = RendererService.getName(result.getClass()); final List renderers = RendererService.getRenderersExcludingLegacyRenderers(name); @@ -358,7 +360,7 @@ private void updatePreviewImage() { return; } - SwingWorker sw = new SwingWorker() { + MultiSwingWorker sw = new MultiSwingWorker() { @Override protected Void doInBackground() throws Exception { @@ -393,7 +395,7 @@ public void done() { main.repaint(); } }; - sw.execute(); + sw.start(); } } } diff --git a/src/main/java/com/rapidminer/gui/processeditor/search/OperatorGlobalSearchGUIProvider.java b/src/main/java/com/rapidminer/gui/processeditor/search/OperatorGlobalSearchGUIProvider.java index 1b387e958..59450418d 100644 --- a/src/main/java/com/rapidminer/gui/processeditor/search/OperatorGlobalSearchGUIProvider.java +++ b/src/main/java/com/rapidminer/gui/processeditor/search/OperatorGlobalSearchGUIProvider.java @@ -37,7 +37,6 @@ import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; -import javax.swing.SwingWorker; import javax.swing.border.Border; import javax.swing.event.AncestorEvent; import javax.swing.event.AncestorListener; @@ -51,6 +50,7 @@ import com.rapidminer.gui.flow.processrendering.model.ProcessRendererModel; import com.rapidminer.gui.search.GlobalSearchGUIUtilities; import com.rapidminer.gui.search.GlobalSearchableGUIProvider; +import com.rapidminer.gui.tools.MultiSwingWorker; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorCreationException; @@ -234,7 +234,7 @@ public void searchResultBrowsed(final Document document) { return; } - new SwingWorker() { + new MultiSwingWorker() { @Override protected Void doInBackground() { @@ -246,7 +246,7 @@ protected Void doInBackground() { } return null; } - }.execute(); + }.start(); } @Override diff --git a/src/main/java/com/rapidminer/gui/properties/AdditionalPermissionsListener.java b/src/main/java/com/rapidminer/gui/properties/AdditionalPermissionsListener.java index 4093e5e72..3d6d4ee90 100644 --- a/src/main/java/com/rapidminer/gui/properties/AdditionalPermissionsListener.java +++ b/src/main/java/com/rapidminer/gui/properties/AdditionalPermissionsListener.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ package com.rapidminer.gui.properties; import java.awt.Dialog.ModalityType; diff --git a/src/main/java/com/rapidminer/gui/properties/AttributeOrderingDialog.java b/src/main/java/com/rapidminer/gui/properties/AttributeOrderingDialog.java index 5bb2a64d9..95bb3c806 100644 --- a/src/main/java/com/rapidminer/gui/properties/AttributeOrderingDialog.java +++ b/src/main/java/com/rapidminer/gui/properties/AttributeOrderingDialog.java @@ -26,6 +26,7 @@ import com.rapidminer.gui.tools.dialogs.InputDialog; import com.rapidminer.parameter.ParameterTypeAttributeOrderingRules; import com.rapidminer.tools.I18N; +import com.rapidminer.tools.container.Pair; import java.awt.Color; import java.awt.Component; @@ -35,17 +36,17 @@ import java.awt.GridLayout; import java.awt.Insets; import java.awt.event.ActionEvent; +import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; +import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; - +import java.util.stream.Collectors; import javax.swing.Action; import javax.swing.DefaultListCellRenderer; import javax.swing.DefaultListModel; @@ -67,9 +68,9 @@ public class AttributeOrderingDialog extends PropertyDialog { private static final long serialVersionUID = 5396725165122306231L; - private final ArrayList attributes; + private final List attributes; - private final ArrayList selectedRules; + private final List selectedRules; private final FilterTextField attributeSearchField; @@ -85,7 +86,7 @@ public class AttributeOrderingDialog extends PropertyDialog { private FilterCondition currentTextFieldCondition; - private final Map ruleToConditionMap = new HashMap(); + private final Map ruleToConditionMap = new HashMap<>(); private final Action selectAttributesAction = new ResourceAction(true, "attribute_ordering.attributes_select") { @@ -95,9 +96,9 @@ public class AttributeOrderingDialog extends PropertyDialog { public void loggedActionPerformed(ActionEvent e) { int[] indices = attributeList.getSelectedIndices(); attributeList.setSelectedIndices(new int[] {}); - List selectedItems = new LinkedList(); + List selectedItems = new LinkedList<>(); for (int i = 0; i < indices.length; i++) { - selectedItems.add(attributeListModel.getElementAt(indices[i]).toString()); + selectedItems.add(attributeListModel.getElementAt(indices[i])); } for (String item : selectedItems) { @@ -121,9 +122,9 @@ public void loggedActionPerformed(ActionEvent e) { public void loggedActionPerformed(ActionEvent e) { int[] indices = selectedRulesList.getSelectedIndices(); selectedRulesList.setSelectedIndices(new int[] {}); - List selectedItems = new LinkedList(); + List selectedItems = new LinkedList<>(); for (int i = 0; i < indices.length; i++) { - selectedItems.add(selectedRulesListModel.getElementAt(indices[i]).toString()); + selectedItems.add(selectedRulesListModel.getElementAt(indices[i])); } for (String item : selectedItems) { @@ -187,8 +188,8 @@ public void loggedActionPerformed(ActionEvent e) { // bubble sort rules list model int currentIndex = selectedIndices[i]; - String movedDown = selectedRulesListModel.get(currentIndex - 1).toString(); - String movedUp = selectedRulesListModel.get(currentIndex).toString(); + String movedDown = selectedRulesListModel.get(currentIndex - 1); + String movedUp = selectedRulesListModel.get(currentIndex); selectedRulesListModel.set(currentIndex, movedDown); selectedRulesListModel.set(currentIndex - 1, movedUp); @@ -222,8 +223,8 @@ public void loggedActionPerformed(ActionEvent e) { // bubble sort rules list model int currentIndex = selectedIndices[i] + 1; - String movedDown = selectedRulesListModel.get(currentIndex - 1).toString(); - String movedUp = selectedRulesListModel.get(currentIndex).toString(); + String movedDown = selectedRulesListModel.get(currentIndex - 1); + String movedUp = selectedRulesListModel.get(currentIndex); selectedRulesListModel.set(currentIndex, movedDown); selectedRulesListModel.set(currentIndex - 1, movedUp); @@ -287,17 +288,14 @@ public AttributeOrderingDialog(final ParameterTypeAttributeOrderingRules type, C JPanel panel = new JPanel(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints(); - attributes = new ArrayList(); - selectedRules = new ArrayList(); + attributes = type.getAttributeNamesAndTypes(false).stream().map(Pair::getFirst) + .filter(name -> !preselectedItems.contains(name)).collect(Collectors.toList()); + attributes.sort(FilterableListModel.STRING_COMPARATOR); + attributeListModel = new FilterableListModel<>(attributes, false); + attributeListModel.setComparator(FilterableListModel.STRING_COMPARATOR); - attributeListModel = new FilterableListModel<>(); + selectedRules = new ArrayList<>(); selectedRulesListModel = new DefaultListModel<>(); - for (String item : type.getAttributeNames()) { - if (item != null && item.trim().length() != 0 && !preselectedItems.contains(item)) { - attributes.add(item); - attributeListModel.addElement(item); - } - } if (!preselectedItems.isEmpty()) { for (String item : preselectedItems) { if (item != null && item.trim().length() != 0) { @@ -345,7 +343,7 @@ public void loggedActionPerformed(ActionEvent e) { itemSearchFieldPanel.add(hideMatchedButton, c); attributeList = new JList<>(attributeListModel); - attributeList.addMouseListener(new MouseListener() { + attributeList.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { @@ -353,18 +351,6 @@ public void mouseClicked(MouseEvent e) { selectAttributesAction.actionPerformed(null); } } - - @Override - public void mouseEntered(MouseEvent e) {} - - @Override - public void mouseExited(MouseEvent e) {} - - @Override - public void mousePressed(MouseEvent e) {} - - @Override - public void mouseReleased(MouseEvent e) {} }); attributeList.setCellRenderer(new ListCellRenderer() { @@ -374,7 +360,7 @@ public void mouseReleased(MouseEvent e) {} public Component getListCellRendererComponent(JList list, String value, int index, boolean isSelected, boolean cellHasFocus) { Component renderComp = renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - if (currentTextFieldCondition != null && currentTextFieldCondition.matches(value.toString())) { + if (currentTextFieldCondition != null && currentTextFieldCondition.matches(value)) { renderComp.setForeground(Color.red); } else { renderComp.setForeground(Color.black); @@ -414,10 +400,7 @@ public Component getListCellRendererComponent(JList list, Stri c.fill = GridBagConstraints.BOTH; addRuleTextField = new JTextField(); - addRuleTextField.addKeyListener(new KeyListener() { - - @Override - public void keyTyped(KeyEvent e) {} + addRuleTextField.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { @@ -433,9 +416,6 @@ public void keyReleased(KeyEvent e) { } } - - @Override - public void keyPressed(KeyEvent e) {} }); addRulePanel.add(addRuleTextField, c); @@ -455,7 +435,7 @@ public void keyPressed(KeyEvent e) {} JPanel orderingListAndButtonContainer = new JPanel(new GridBagLayout()); selectedRulesList = new JList<>(selectedRulesListModel); - selectedRulesList.addMouseListener(new MouseListener() { + selectedRulesList.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { @@ -463,18 +443,6 @@ public void mouseClicked(MouseEvent e) { deselectAttributesAction.actionPerformed(null); } } - - @Override - public void mouseEntered(MouseEvent e) {} - - @Override - public void mouseExited(MouseEvent e) {} - - @Override - public void mousePressed(MouseEvent e) {} - - @Override - public void mouseReleased(MouseEvent e) {} }); JScrollPane selectedRulesListPane = new ExtendedJScrollPane(selectedRulesList); selectedRulesListPane.setBorder(createBorder()); diff --git a/src/main/java/com/rapidminer/gui/properties/AttributesPropertyDialog.java b/src/main/java/com/rapidminer/gui/properties/AttributesPropertyDialog.java index f8f373634..e6e646eb8 100644 --- a/src/main/java/com/rapidminer/gui/properties/AttributesPropertyDialog.java +++ b/src/main/java/com/rapidminer/gui/properties/AttributesPropertyDialog.java @@ -25,13 +25,13 @@ import java.awt.GridLayout; import java.awt.Insets; import java.awt.event.ActionEvent; +import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import javax.swing.Action; import javax.swing.DefaultListCellRenderer; import javax.swing.Icon; @@ -61,9 +61,9 @@ public class AttributesPropertyDialog extends PropertyDialog { private static final long serialVersionUID = 5396725165122306231L; - private final ArrayList items; + private final List items; - private final ArrayList selectedItems; + private final List selectedItems; private final Map valueTypeMap; @@ -105,7 +105,7 @@ public void loggedActionPerformed(ActionEvent e) { int[] indices = selectedItemList.getSelectedIndices(); selectedItemList.setSelectedIndices(new int[] {}); for (int i = indices.length - 1; i >= 0; i--) { - String item = selectedItemListModel.getElementAt(indices[i]).toString(); + String item = selectedItemListModel.getElementAt(indices[i]); itemListModel.addElement(item); selectedItemListModel.removeElementAt(indices[i]); items.add(item); @@ -136,22 +136,20 @@ public AttributesPropertyDialog(final ParameterTypeAttributes type, Collection(); - selectedItems = new ArrayList<>(); - valueTypeMap = new HashMap<>(); - - itemListModel = new FilterableListModel<>(true); - selectedItemListModel = new FilterableListModel<>(sortAttributes); - for (Pair item : type.getAttributeNamesAndTypes(sortAttributes)) { - if (!preselectedItems.contains(item.getFirst())) { - items.add(item.getFirst()); - itemListModel.addElement(item.getFirst()); - } - valueTypeMap.put(item.getFirst(), item.getSecond()); + List> attributeNamesAndTypes = type.getAttributeNamesAndTypes(false); + items = attributeNamesAndTypes.stream().map(Pair::getFirst).filter(att -> !preselectedItems.contains(att)).collect(Collectors.toList()); + valueTypeMap = attributeNamesAndTypes.stream().collect(Collectors.toMap(Pair::getFirst, Pair::getSecond)); + selectedItems = new ArrayList<>(preselectedItems); + if (sortAttributes) { + items.sort(FilterableListModel.STRING_COMPARATOR); + selectedItems.sort(FilterableListModel.STRING_COMPARATOR); } - for (String item : preselectedItems) { - selectedItems.add(item); - selectedItemListModel.addElement(item); + + itemListModel = new FilterableListModel<>(items, false); + selectedItemListModel = new FilterableListModel<>(selectedItems, false); + if (sortAttributes) { + itemListModel.setComparator(FilterableListModel.STRING_COMPARATOR); + selectedItemListModel.setComparator(FilterableListModel.STRING_COMPARATOR); } itemSearchField = new FilterTextField(); @@ -181,7 +179,7 @@ public void loggedActionPerformed(ActionEvent e) { itemList = new JList<>(itemListModel); itemList.setCellRenderer(createAttributeTypeListRenderer()); - itemList.addMouseListener(new MouseListener() { + itemList.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { @@ -189,18 +187,6 @@ public void mouseClicked(MouseEvent e) { selectAttributesAction.actionPerformed(null); } } - - @Override - public void mouseEntered(MouseEvent e) {} - - @Override - public void mouseExited(MouseEvent e) {} - - @Override - public void mousePressed(MouseEvent e) {} - - @Override - public void mouseReleased(MouseEvent e) {} }); JScrollPane itemListPane = new ExtendedJScrollPane(itemList); itemListPane.setBorder(createBorder()); @@ -274,7 +260,7 @@ public void loggedActionPerformed(ActionEvent e) { selectedItemList = new JList<>(selectedItemListModel); selectedItemList.setCellRenderer(createAttributeTypeListRenderer()); - selectedItemList.addMouseListener(new MouseListener() { + selectedItemList.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { @@ -282,18 +268,6 @@ public void mouseClicked(MouseEvent e) { deselectAttributesAction.actionPerformed(null); } } - - @Override - public void mouseEntered(MouseEvent e) {} - - @Override - public void mouseExited(MouseEvent e) {} - - @Override - public void mousePressed(MouseEvent e) {} - - @Override - public void mouseReleased(MouseEvent e) {} }); JScrollPane selectedItemListPane = new ExtendedJScrollPane(selectedItemList); selectedItemListPane.setBorder(createBorder()); diff --git a/src/main/java/com/rapidminer/gui/properties/ExpressionPropertyDialog.java b/src/main/java/com/rapidminer/gui/properties/ExpressionPropertyDialog.java index 247e8e77a..39d6776fa 100644 --- a/src/main/java/com/rapidminer/gui/properties/ExpressionPropertyDialog.java +++ b/src/main/java/com/rapidminer/gui/properties/ExpressionPropertyDialog.java @@ -36,7 +36,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; - import javax.swing.AbstractButton; import javax.swing.BorderFactory; import javax.swing.Icon; @@ -106,6 +105,17 @@ public class ExpressionPropertyDialog extends PropertyDialog { private static final long serialVersionUID = 5567661137372752202L; + /** + * The maximum number of characters allowed in the syntax error message (without the TRUNCATED_SYMBOL to mark the + * truncated parts). + */ + private static final int MAX_ERROR_MESSAGE_LENGTH = 124; + + /** + * This string will be used to mark places in the syntax error message that have been truncated. + */ + private static final String TRUNCATED_SYMBOL = "[...]"; + /** * An input panel owns an {@link Observer}, which is updated about model changes. * @@ -1417,32 +1427,86 @@ private void showError(boolean error, String title, String message) { // DO NOT CHANGE THIS, AS THE INDENTATION IS WRONG OTHERWISE validationTextArea.setFont(FontTools.getFont(Font.MONOSPACED, Font.PLAIN, 12)); - // set the error message - // strip the error message if necessary if (splittedMessage.length > 1) { - int buffer = 50; - int stepsize = 5; - int stringWidth = SwingTools.getStringWidth(validationTextArea, splittedMessage[1]) + buffer; - boolean cut = false; - while (stringWidth > validationTextArea.getSize().width - stepsize) { - cut = true; - splittedMessage[1] = splittedMessage[1].substring(stepsize, splittedMessage[1].length()); - if (splittedMessage.length > 2) { - splittedMessage[2] = splittedMessage[2].substring(stepsize, splittedMessage[2].length()); - } - stringWidth = SwingTools.getStringWidth(validationTextArea, splittedMessage[1]) + buffer; - } - if (cut) { - splittedMessage[1] = "[...]" + splittedMessage[1]; - if (splittedMessage.length > 2) { - splittedMessage[2] = " " + splittedMessage[2]; - } - } + // truncate error message to maxMessageLength where necessary to avoid overflow in the validationTextArea + truncateMessage(splittedMessage, MAX_ERROR_MESSAGE_LENGTH); } + // set the error message String errorMessage = splittedMessage.length > 1 ? splittedMessage[1] : "\n"; if (splittedMessage.length > 2) { errorMessage += "\n" + splittedMessage[2]; } validationTextArea.setText(errorMessage); } + + /** + * Helper method that truncates the given error message if it is longer than maxChar characters. It does not return + * a new string array but modifies the given one instead. In this case the + * error message will contain as much of the error as possible plus a symmetrical window around it. Parts of the + * original message that have been truncated are marked by the TRUNCATED_SYMBOL constant string. + * + * @param splittedMessage + * the original error message + * @param maxChars + * the maximum number of characters the error message is allowed to have + */ + static void truncateMessage(String[] splittedMessage, int maxChars) { + if (splittedMessage.length > 2) { + // that means the error has been marked. we dont want to cut the error away + if (maxChars < splittedMessage[1].length()) { + // every character of the error is marked by a '^' in splittedMessage[2] + int errorStart = splittedMessage[2].indexOf('^'); + int errorEnd = splittedMessage[2].lastIndexOf('^'); + int errorLength = errorEnd - errorStart + 1; + int originalMsgLength = splittedMessage[1].length(); + if (errorLength > maxChars) { + // the end of the error has to be cut because there is no space for the whole error + splittedMessage[1] = splittedMessage[1].substring(errorStart, errorStart + maxChars); + splittedMessage[2] = splittedMessage[2].substring(errorStart, errorStart + maxChars); + addTruncatedSymbols(splittedMessage, errorStart, errorEnd, originalMsgLength); + } else { + // we can preserve the whole error. we will print the error and a symmetrical window around it + int rest = maxChars - errorLength; + int windowStart = errorStart - (rest / 2); + int windowEnd = errorEnd + (rest / 2); + // now we need to make sure that the window is inside the strings bounds + if (windowStart < 0) { + // pushes the window into the strings bounds from the left side + windowEnd -= windowStart; + windowStart = 0; + } + if (windowEnd >= originalMsgLength) { + // pushes the window into the strings bounds from the right side + windowStart -= windowEnd - originalMsgLength + 1; + windowEnd = originalMsgLength - 1; + } + // applies the windows to the error message + splittedMessage[1] = splittedMessage[1].substring(windowStart, windowEnd + 1); + splittedMessage[2] = splittedMessage[2].substring(windowStart, Math.min(windowEnd + 1, + splittedMessage[2].length())); + // adds the TRUNCATED_SYMBOL where necessary + addTruncatedSymbols(splittedMessage, windowStart, windowEnd, originalMsgLength); + } + } + + } else { + // that means the error has not been marked. therefore we simply cut the end if necessary + if (maxChars < splittedMessage[1].length()) { + splittedMessage[1] = splittedMessage[1].substring(0, maxChars) + TRUNCATED_SYMBOL; + } + } + } + + /** + * Helper method that adds the TRUNCATED_SYMBOL constant string to the error message where needed + */ + private static void addTruncatedSymbols(String[] splittedMessage, int windowStart, int windowEnd, int originalMsgLength) { + if (windowStart > 0) { + splittedMessage[1] = TRUNCATED_SYMBOL + splittedMessage[1]; + splittedMessage[2] = " " + splittedMessage[2]; + } + if (windowEnd < (originalMsgLength - 1)) { + splittedMessage[1] = splittedMessage[1] + TRUNCATED_SYMBOL; + } + } } diff --git a/src/main/java/com/rapidminer/gui/properties/MacroSelectionDialog.java b/src/main/java/com/rapidminer/gui/properties/MacroSelectionDialog.java index c736d0f42..8cdbb3634 100644 --- a/src/main/java/com/rapidminer/gui/properties/MacroSelectionDialog.java +++ b/src/main/java/com/rapidminer/gui/properties/MacroSelectionDialog.java @@ -28,7 +28,6 @@ import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; - import javax.swing.BorderFactory; import javax.swing.JDialog; import javax.swing.JLabel; @@ -70,6 +69,12 @@ public class MacroSelectionDialog extends JDialog { /** indicator if the expression part is highlighted */ private boolean expressionHighlighted = false; + /** panel showing the 'Insert as evaluated attribute' part */ + private JPanel attributePanel = new JPanel(); + + /** indicator if the attribute part is highlighted */ + private boolean attributeHighlighted = false; + /** indicates if the old macro handling is used */ private boolean deprecated = false; @@ -124,6 +129,22 @@ public class MacroSelectionDialog extends JDialog { /** end of the expression, when the user selects the expression part */ private static final String EXPRESSION_CALL_END_DEPRECATED = "}"; + /** title of the attribute part */ + private static final String ATTRIBUTE_TITLE = I18N.getGUILabel("macro_selection_dialog.attribute.title"); + + /** description of the attribute part 1 */ + private static final String ATTRIBUTE_DESCRIPTION = I18N.getGUILabel("macro_selection_dialog.attribute" + + ".description"); + + /** expression, when the user selects the attribute part */ + private static final String ATTRIBUTE_CALL = "#{macro_name}"; + + /** start of the expression, when the user selects the attribute part */ + private static final String ATTRIBUTE_CALL_START = "#{"; + + /** end of the expression, when the user selects the attribute part */ + private static final String ATTRIBUTE_CALL_END = "}"; + /** * Creates a dialog for a given {@link FunctionInputType#MACRO} to choose between inserting the * macro as a pure value or as an interpreted expression. @@ -168,7 +189,7 @@ private void initGui(final FunctionInputPanel macroPanel) { setModalityType(ModalityType.APPLICATION_MODAL); setTitle(DIALOG_TITLE); setIconImage(SwingTools.createIcon("16/rapidminer_studio.png").getImage()); - setSize(new Dimension(300, 300)); + setSize(new Dimension(300, 375)); JPanel main = new JPanel(); GridBagLayout mainLayout = new GridBagLayout(); @@ -186,6 +207,7 @@ private void initGui(final FunctionInputPanel macroPanel) { valuePanel.setLayout(new GridBagLayout()); valuePanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); valuePanel.setBorder(BorderFactory.createMatteBorder(1, 1, 1, 1, Color.GRAY)); + gbc.insets = new Insets(5, 3, 2, 0); // add text if (deprecated) { valuePanel.add(new JLabel("

" + VALUE_TITLE @@ -217,6 +239,7 @@ public void mouseClicked(MouseEvent e) { public void mouseEntered(MouseEvent e) { highlightValue(true); highlightExpression(false); + highlightAttribute(false); } @Override @@ -224,7 +247,7 @@ public void mouseExited(MouseEvent e) { highlightValue(false); } }); - + gbc.insets = new Insets(0, 0, 0, 0); main.add(valuePanel, gbc); // EVALUATED EXPRESSION PART @@ -232,6 +255,7 @@ public void mouseExited(MouseEvent e) { expressionPanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); expressionPanel.setBorder(BorderFactory.createMatteBorder(1, 1, 1, 1, Color.GRAY)); // add text + gbc.insets = new Insets(5, 3, 2, 0); if (deprecated) { expressionPanel.add(new JLabel("

" + EXPRESSION_TITLE + "
" + EXPRESSION_CALL_DEPRECATED + "

" @@ -261,6 +285,7 @@ public void mouseClicked(MouseEvent e) { public void mouseEntered(MouseEvent e) { highlightExpression(true); highlightValue(false); + highlightAttribute(false); } @Override @@ -272,6 +297,45 @@ public void mouseExited(MouseEvent e) { gbc.insets = new Insets(10, 0, 0, 0); main.add(expressionPanel, gbc); + // EVALUATED ATTRIBUTE PART + + attributePanel.setLayout(new GridBagLayout()); + attributePanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + attributePanel.setBorder(BorderFactory.createMatteBorder(1, 1, 1, 1, Color.GRAY)); + // add text + gbc.insets = new Insets(5, 3, 2, 0); + attributePanel.add(new JLabel("

" + ATTRIBUTE_TITLE + + "
" + ATTRIBUTE_CALL + "

" + + ATTRIBUTE_DESCRIPTION + "

"), gbc); + + + // add highlighting behavior and store the expression if the user selects a type of + // expression + attributePanel.addMouseListener(new MouseAdapter() { + + @Override + public void mouseClicked(MouseEvent e) { + expression = ATTRIBUTE_CALL_START + escape(macroPanel.getInputName()) + ATTRIBUTE_CALL_END; + MacroSelectionDialog.this.setVisible(false); + } + + @Override + public void mouseEntered(MouseEvent e) { + highlightAttribute(true); + highlightExpression(false); + highlightValue(false); + } + + @Override + public void mouseExited(MouseEvent e) { + highlightAttribute(false); + } + }); + gbc.gridy += 1; + gbc.insets = new Insets(10, 0, 0, 0); + main.add(attributePanel, gbc); + + // add highlighting behavior for keys and store the expression if the user selects a type of // expression addKeyListener(new KeyAdapter() { @@ -282,16 +346,22 @@ public void keyPressed(KeyEvent e) { if (expressionHighlighted) { highlightExpression(false); highlightValue(true); - } else if (!valueHighlighted) { + } else if (attributeHighlighted) { highlightExpression(true); + highlightAttribute(false); + } else if (!valueHighlighted) { + highlightAttribute(true); highlightValue(false); } } else if (e.getKeyCode() == KeyEvent.VK_DOWN) { if (valueHighlighted) { highlightExpression(true); highlightValue(false); - } else if (!expressionHighlighted) { + } else if (expressionHighlighted) { highlightExpression(false); + highlightAttribute(true); + } else if (!attributeHighlighted) { + highlightAttribute(false); highlightValue(true); } } else if (e.getKeyCode() == KeyEvent.VK_ENTER) { @@ -311,6 +381,9 @@ public void keyPressed(KeyEvent e) { expression = EXPRESSION_CALL_START + escape(macroPanel.getInputName()) + EXPRESSION_CALL_END; } MacroSelectionDialog.this.setVisible(false); + } else if (attributeHighlighted) { + expression = ATTRIBUTE_CALL_START + escape(macroPanel.getInputName()) + ATTRIBUTE_CALL_END; + MacroSelectionDialog.this.setVisible(false); } } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { setVisible(false); @@ -373,4 +446,23 @@ private void highlightExpression(boolean highlight) { } } + /** + * Highlights the part to select, if the user wants to insert the attribute associated to the macro content + * + * @param highlight + * whether it should be highlighted + */ + private void highlightAttribute(boolean highlight) { + + attributeHighlighted = highlight; + + if (highlight) { + attributePanel.setBorder(BorderFactory.createMatteBorder(1, 1, 1, 1, SwingTools.RAPIDMINER_ORANGE)); + attributePanel.setBackground(Color.LIGHT_GRAY); + } else { + + attributePanel.setBorder(BorderFactory.createMatteBorder(1, 1, 1, 1, Color.GRAY)); + attributePanel.setBackground(defaultBackground); + } + } } diff --git a/src/main/java/com/rapidminer/gui/properties/PropertyPanel.java b/src/main/java/com/rapidminer/gui/properties/PropertyPanel.java index 5555240fe..0d5e96208 100644 --- a/src/main/java/com/rapidminer/gui/properties/PropertyPanel.java +++ b/src/main/java/com/rapidminer/gui/properties/PropertyPanel.java @@ -55,6 +55,7 @@ import com.rapidminer.gui.properties.celleditors.value.AttributeOrderingCellEditor; import com.rapidminer.gui.properties.celleditors.value.AttributeValueCellEditor; import com.rapidminer.gui.properties.celleditors.value.AttributesValueCellEditor; +import com.rapidminer.gui.properties.celleditors.value.ConnectionLocationValueCellEditor; import com.rapidminer.gui.properties.celleditors.value.ColorValueCellEditor; import com.rapidminer.gui.properties.celleditors.value.ConfigurableValueCellEditor; import com.rapidminer.gui.properties.celleditors.value.ConfigurationWizardValueCellEditor; @@ -92,6 +93,7 @@ import com.rapidminer.parameter.ParameterTypeAttributeOrderingRules; import com.rapidminer.parameter.ParameterTypeAttributes; import com.rapidminer.parameter.ParameterTypeBoolean; +import com.rapidminer.parameter.ParameterTypeConnectionLocation; import com.rapidminer.parameter.ParameterTypeCategory; import com.rapidminer.parameter.ParameterTypeChar; import com.rapidminer.parameter.ParameterTypeColor; @@ -225,6 +227,7 @@ public void removePropertyEditorDecorator(PropertyEditorDecorator d) { registerPropertyValueCellEditor(ParameterTypeFile.class, SimpleFileValueCellEditor.class); registerPropertyValueCellEditor(ParameterTypeRepositoryLocation.class, RepositoryLocationValueCellEditor.class); registerPropertyValueCellEditor(ParameterTypeProcessLocation.class, ProcessLocationValueCellEditor.class); + registerPropertyValueCellEditor(ParameterTypeConnectionLocation.class, ConnectionLocationValueCellEditor.class); registerPropertyValueCellEditor(ParameterTypeValue.class, OperatorValueValueCellEditor.class); registerPropertyValueCellEditor(ParameterTypeInnerOperator.class, InnerOperatorValueCellEditor.class); registerPropertyValueCellEditor(ParameterTypeList.class, ListValueCellEditor.class); diff --git a/src/main/java/com/rapidminer/gui/properties/RegexpPropertyDialog.java b/src/main/java/com/rapidminer/gui/properties/RegexpPropertyDialog.java index 3ddaf7ed0..1da27e5d9 100644 --- a/src/main/java/com/rapidminer/gui/properties/RegexpPropertyDialog.java +++ b/src/main/java/com/rapidminer/gui/properties/RegexpPropertyDialog.java @@ -1,917 +1,933 @@ -/** - * Copyright (C) 2001-2019 by RapidMiner and the contributors - * - * Complete list of developers available at our web site: - * - * http://rapidminer.com - * - * This program is free software: you can redistribute it and/or modify it under the terms of the - * GNU Affero General Public License as published by the Free Software Foundation, either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License along with this program. - * If not, see http://www.gnu.org/licenses/. -*/ -package com.rapidminer.gui.properties; - -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.ItemEvent; -import java.awt.event.ItemListener; -import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.util.Collection; -import java.util.logging.Level; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; - -import javax.swing.Action; -import javax.swing.BorderFactory; -import javax.swing.DefaultListCellRenderer; -import javax.swing.DefaultListModel; -import javax.swing.Icon; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTabbedPane; -import javax.swing.JTextField; -import javax.swing.JTextPane; -import javax.swing.ListSelectionModel; -import javax.swing.SwingConstants; -import javax.swing.border.Border; -import javax.swing.text.AttributeSet; -import javax.swing.text.BadLocationException; -import javax.swing.text.DefaultStyledDocument; -import javax.swing.text.SimpleAttributeSet; -import javax.swing.text.Style; -import javax.swing.text.StyleConstants; - -import org.apache.commons.lang.ArrayUtils; - -import com.rapidminer.gui.ApplicationFrame; -import com.rapidminer.gui.LoggedAbstractAction; -import com.rapidminer.gui.tools.ExtendedJScrollPane; -import com.rapidminer.gui.tools.SwingTools; -import com.rapidminer.gui.tools.components.PlainArrowDropDownButton; -import com.rapidminer.gui.tools.dialogs.ButtonDialog; -import com.rapidminer.tools.I18N; -import com.rapidminer.tools.LogService; -import com.rapidminer.tools.Tools; - -import groovy.swing.impl.DefaultAction; - - -/** - * A dialog to create and edit regular expressions. Can be created with a given predefined regular - * expression (normally a previously set value). A collection of item strings can be given to the - * dialog which are then available as shortcuts. Additionally, a list shows which of these items - * match the regular expression. If the item collection is null, both lists will not be visible. - * - * The dialog shows an inline preview displaying where the given pattern matches. It also shows a - * list of matches, together with their matching groups. - * - * @author Tobias Malbrecht, Dominik Halfkann, Simon Fischer - */ -public class RegexpPropertyDialog extends ButtonDialog { - - private static final long serialVersionUID = 5396725165122306231L; - - private RegexpSearchStyledDocument inlineSearchDocument = null; - private RegexpReplaceStyledDocument inlineReplaceDocument = null; - - private JTabbedPane testExp = null; - - private DefaultListModel resultsListModel = new DefaultListModel(); - - private static String[][] regexpConstructs = { - { ".", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.any_character") }, - { "[]", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.bracket_expression") }, - { "[^]", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.not_bracket_expression") }, - { "()", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.capturing_group") }, - { "?", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.zero_one_quantifier") }, - { "*", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.zero_more_quantifier") }, - { "+", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.one_more_quantifier") }, - { "{n}", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.exact_quantifier") }, - { "{min,}", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.min_quantifier") }, - { "{min,max}", - I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.min_max_quantifier") }, - { "|", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.disjunction") }, }; - - // adjust the caret by this amount upon insertion - private static int[] regexpConstructInsertionCaretAdjustment = { 0, -1, -1, -1, 0, 0, 0, -1, -2, -1, -5, 0, }; - - // select these construct characters upon insertion - private static int[][] regexpConstructInsertionSelectionIndices = { { 1, 1 }, { 1, 1 }, { 2, 2 }, { 1, 1 }, { 1, 1 }, - { 1, 1 }, { 1, 1 }, { 1, 2 }, { 1, 4 }, { 1, 8 }, { 1, 1 }, }; - - // enclose selected by construct - private static boolean[] regexpConstructInsertionEncloseSelected = { false, true, true, true, false, false, false, false, - false, false, false, }; - - private static String[][] regexpShortcuts = { - { ".*", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.arbitrary") }, - { "[a-zA-Z]", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.letter") }, - { "[a-z]", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.lowercase_letter") }, - { "[A-Z]", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.uppercase_letter") }, - { "[0-9]", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.digit") }, - { "\\w", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.word") }, - { "\\W", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.non_word") }, - { "\\s", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.whitespace") }, - { "\\S", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.non_whitespace") }, - { "[-!\"#$%&'()*+,./:;<=>?@\\[\\\\\\]_`{|}~]", - I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.punctuation") }, }; - - private static final String ERROR_MESSAGE = I18N.getMessage(I18N.getGUIBundle(), - "gui.dialog.parameter.regexp.error.label"); - - private static final Icon ERROR_ICON = SwingTools - .createIcon("16/" + I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.error.icon")); - - private static final String NO_ERROR_MESSAGE = I18N.getMessage(I18N.getGUIBundle(), - "gui.dialog.parameter.regexp.no_error.label"); - - private static final Icon NO_ERROR_ICON = SwingTools - .createIcon("16/" + I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.no_error.icon")); - - private String infoText; - - private final JTextField regexpTextField; - - private final JTextField replacementTextField; - - private JList itemShortcutsList; - - private DefaultListModel matchedItemsListModel; - - private final Collection items; - - private boolean supportsItems = false; - - private final JLabel errorMessage; - - private JButton okButton; - - JCheckBox cbCaseInsensitive; - JCheckBox cbComments; - JCheckBox cbMultiline; - JCheckBox cbDotall; - JCheckBox cbUnicodeCase; - - /** Class representing a single regexp search result. **/ - private class RegExpResult { - - private String match; - private String[] groups; - private int number; - private boolean empty = false; - - public RegExpResult(String match, String[] groups, int number) { - this.match = match; - this.groups = groups; - this.number = number; - } - - public RegExpResult() { - // empty result - empty = true; - } - - @Override - public String toString() { - String output = ""; - if (!empty) { - output += "" + "" + - // "Match "+number+": '"+match+"'" + - I18N.getMessage(I18N.getGUIBundle(), - "gui.dialog.parameter.regexp.regular_expression.result_list.match", number, - "'" + Tools.escapeHTML(match) + "'") - + ""; - if (groups.length > 0) { - output += "
    "; - for (int i = 0; i < groups.length; i++) { - // output += "
  1. Group matches: '" + groups[i] +"'
  2. "; - output += "
  3. " + I18N.getMessage(I18N.getGUIBundle(), - "gui.dialog.parameter.regexp.regular_expression.result_list.group_match", - "'" + Tools.escapeHTML(groups[i]) + "'") + "
  4. "; - - } - output += ""; - } - output += ""; - } else { - output += "" + "" + I18N.getMessage( - I18N.getGUIBundle(), "gui.dialog.parameter.regexp.regular_expression.result_list.empty") + ""; - output += ""; - } - return output; - } - } - - /** A StyledDocument providing a live regexp search **/ - private class RegexpSearchStyledDocument extends DefaultStyledDocument { - - private static final long serialVersionUID = 1L; - - private Matcher matcher = Pattern.compile("").matcher(""); - - Style keyStyle; - Style rootStyle; - - { - rootStyle = addStyle("root", null); - - keyStyle = addStyle("key", rootStyle); - StyleConstants.setBackground(keyStyle, Color.YELLOW); - } - - @Override - public void insertString(int offs, String str, AttributeSet a) throws BadLocationException { - super.insertString(offs, str, a); - checkDocument(); - } - - @Override - public void remove(int offs, int len) throws BadLocationException { - super.remove(offs, len); - checkDocument(); - } - - private void checkDocument() { - - setCharacterAttributes(0, getLength(), rootStyle, true); - try { - matcher.reset(getText(0, getLength())); - int count = 0; - resultsListModel.clear(); - while (matcher.find()) { - if (matcher.end() <= matcher.start()) { - continue; - } - setCharacterAttributes(matcher.start(), matcher.end() - matcher.start(), keyStyle, true); - - String[] groups = new String[matcher.groupCount()]; - for (int i = 1; i <= matcher.groupCount(); i++) { - groups[i - 1] = matcher.group(i); - } - resultsListModel.addElement(new RegExpResult( - this.getText(matcher.start(), matcher.end() - matcher.start()), groups, count + 1)); - count++; - } - - if (count == 0) { - // add empty element - resultsListModel.addElement(new RegExpResult()); - } - - testExp.setTitleAt(1, I18N.getMessage(I18N.getGUIBundle(), - "gui.dialog.parameter.regexp.regular_expression.result_list.title") + " (" + count + ")"); - inlineReplaceDocument.setText(matcher.replaceAll(replacementTextField.getText())); - updateRegexpOptions(); - } catch (BadLocationException ex) { - LogService.getRoot().log(Level.WARNING, RegexpPropertyDialog.class.getName() + ".bad_location", ex); - } - } - - public void updatePattern(String pattern) { - this.matcher = Pattern.compile(pattern).matcher(""); - checkDocument(); - } - - public void clearResults() { - resultsListModel.clear(); - resultsListModel.addElement(new RegExpResult()); - testExp.setTitleAt(1, - I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.regular_expression.result_list.title") - + " (0)"); - setCharacterAttributes(0, getLength(), rootStyle, true); - } - - } - - /** A StyledDocument with an added setText() method for interting the replaced text **/ - private class RegexpReplaceStyledDocument extends DefaultStyledDocument { - - private static final long serialVersionUID = 1L; - - public RegexpReplaceStyledDocument() { - super(); - } - - public void setText(String text) { - try { - remove(0, getLength()); - insertString(0, text, null); - } catch (BadLocationException e) { - LogService.getRoot().log(Level.WARNING, RegexpPropertyDialog.class.getName() + ".bad_location", e); - } - } - } - - public RegexpPropertyDialog(final Collection items, String predefinedRegexp, String description) { - super(ApplicationFrame.getApplicationFrame(), "parameter.regexp", ModalityType.APPLICATION_MODAL, new Object[] {}); - this.items = items; - this.supportsItems = items != null; - this.infoText = "" + I18N.getMessage(I18N.getGUIBundle(), getKey() + ".title") + ":
    " + description - + ""; - Dimension size = new Dimension(420, 500); - this.setMinimumSize(size); - this.setPreferredSize(size); - - JPanel panel = new JPanel(createGridLayout(1, supportsItems ? 2 : 1)); - - // create regexp text field - regexpTextField = new JTextField(predefinedRegexp); - regexpTextField - .setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.regular_expression.tip")); - regexpTextField.addKeyListener(new KeyListener() { - - @Override - public void keyPressed(KeyEvent e) {} - - @Override - public void keyReleased(KeyEvent e) { - fireRegularExpressionUpdated(); - } - - @Override - public void keyTyped(KeyEvent e) {} - - }); - regexpTextField.requestFocus(); - - // create replacement text field - replacementTextField = new JTextField(); - replacementTextField - .setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.replacement.tip")); - replacementTextField.addKeyListener(new KeyListener() { - - @Override - public void keyPressed(KeyEvent e) {} - - @Override - public void keyReleased(KeyEvent e) { - fireRegularExpressionUpdated(); - } - - @Override - public void keyTyped(KeyEvent e) {} - - }); - - // create inline search documents - inlineSearchDocument = new RegexpSearchStyledDocument(); - inlineReplaceDocument = new RegexpReplaceStyledDocument(); - - // create search results list - DefaultListCellRenderer resultCellRenderer = new DefaultListCellRenderer() { - - private static final long serialVersionUID = 1L; - - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, - boolean cellHasFocus) { - super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - setBackground(list.getBackground()); - setForeground(list.getForeground()); - setBorder(getNoFocusBorder()); - return this; - } - - private Border getNoFocusBorder() { - Border border = BorderFactory.createMatteBorder(0, 0, 1, 0, Color.gray); - return border; - } - }; - - JList regexpFindingsList = new JList(resultsListModel); - regexpFindingsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - regexpFindingsList.setLayoutOrientation(JList.VERTICAL); - regexpFindingsList.setCellRenderer(resultCellRenderer); - - // regexp panel on left side of dialog - JPanel regexpPanel = new JPanel(new GridBagLayout()); - regexpPanel.setBorder(createTitledBorder( - I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.regular_expression.border"))); - GridBagConstraints c = new GridBagConstraints(); - c.insets = new Insets(4, 4, 4, 0); - c.gridx = 0; - c.gridy = 0; - c.weightx = 1; - c.fill = GridBagConstraints.BOTH; - regexpPanel.add(regexpTextField, c); - - // make shortcut button - final Action nullAction = new DefaultAction(); - PlainArrowDropDownButton autoWireDropDownButton = PlainArrowDropDownButton.makeDropDownButton(nullAction); - - for (String[] popupItem : (String[][]) ArrayUtils.addAll(regexpConstructs, regexpShortcuts)) { - String shortcut = popupItem[0].length() > 14 ? popupItem[0].substring(0, 14) + "..." : popupItem[0]; - autoWireDropDownButton - .add(new InsertionAction("
    " - + shortcut + "" + popupItem[1] + "
    ", popupItem[0])); - } - c.insets = new Insets(4, 0, 4, 0); - c.gridx = 1; - c.weightx = 0; - c.fill = GridBagConstraints.HORIZONTAL; - regexpPanel.add(autoWireDropDownButton.getDropDownArrowButton(), c); - - // make delete button - c.insets = new Insets(4, 0, 4, 4); - c.gridx = 2; - c.weightx = 0; - c.fill = GridBagConstraints.HORIZONTAL; - JButton clearRegexpTextFieldButton = new JButton(SwingTools.createIcon("16/delete.png")); - clearRegexpTextFieldButton.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - regexpTextField.setText(""); - fireRegularExpressionUpdated(); - regexpTextField.requestFocusInWindow(); - } - }); - - regexpPanel.add(clearRegexpTextFieldButton, c); - - errorMessage = new JLabel(NO_ERROR_MESSAGE, NO_ERROR_ICON, SwingConstants.LEFT); - errorMessage.setFocusable(false); - c.insets = new Insets(4, 8, 4, 4); - c.gridx = 0; - c.gridy = 1; - c.weightx = 0; - c.weighty = 0; - c.gridwidth = GridBagConstraints.REMAINDER; - regexpPanel.add(errorMessage, c); - - // create replacement panel - JPanel replacementPanel = new JPanel(new GridBagLayout()); - replacementPanel.setBorder( - createTitledBorder(I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.replacement.border"))); - - JPanel testerPanel = new JPanel(new GridBagLayout()); - - c.insets = new Insets(4, 4, 4, 0); - c.gridx = 0; - c.gridy = 0; - c.weightx = 1; - c.fill = GridBagConstraints.HORIZONTAL; - replacementPanel.add(replacementTextField, c); - - // create inline search panel - JPanel inlineSearchPanel = new JPanel(new GridBagLayout()); - - c.insets = new Insets(8, 4, 4, 4); - c.gridx = 0; - c.gridy = 0; - c.weightx = 1; - c.weighty = 0; - c.fill = GridBagConstraints.HORIZONTAL; - inlineSearchPanel.add( - new JLabel(I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.inline_search.search")), c); - - c.insets = new Insets(0, 0, 0, 0); - c.gridx = 0; - c.gridy = 1; - c.weightx = 1; - c.weighty = 1; - c.fill = GridBagConstraints.BOTH; - inlineSearchPanel.add(new JScrollPane(new JTextPane(inlineSearchDocument)), c); - - c.insets = new Insets(8, 4, 4, 4); - c.gridx = 0; - c.gridy = 2; - c.weightx = 1; - c.weighty = 0; - c.fill = GridBagConstraints.HORIZONTAL; - inlineSearchPanel.add( - new JLabel(I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.inline_search.replaced")), c); - - c.insets = new Insets(0, 0, 0, 0); - c.gridx = 0; - c.gridy = 3; - c.weightx = 1; - c.weighty = 1; - c.fill = GridBagConstraints.BOTH; - JTextPane replaceTextPane = new JTextPane(inlineReplaceDocument); - replaceTextPane.setEditable(false); - JScrollPane scrollpane = new JScrollPane(replaceTextPane); - scrollpane.setBorder(null); - inlineSearchPanel.add(new JScrollPane(replaceTextPane), c); - - // create regexp options panel - ItemListener defaultOptionListener = new ItemListener() { - - @Override - public void itemStateChanged(ItemEvent e) { - fireRegexpOptionsChanged(); - } - }; - - cbCaseInsensitive = new JCheckBox(I18N.getMessage(I18N.getGUIBundle(), - "gui.dialog.parameter.regexp.regular_expression.regexp_options.case_insensitive")); - cbCaseInsensitive.setToolTipText(I18N.getMessage(I18N.getGUIBundle(), - "gui.dialog.parameter.regexp.regular_expression.regexp_options.case_insensitive.tip")); - cbCaseInsensitive.addItemListener(defaultOptionListener); - - cbMultiline = new JCheckBox(I18N.getMessage(I18N.getGUIBundle(), - "gui.dialog.parameter.regexp.regular_expression.regexp_options.multiline_mode")); - cbMultiline.setToolTipText(I18N.getMessage(I18N.getGUIBundle(), - "gui.dialog.parameter.regexp.regular_expression.regexp_options.multiline_mode.tip")); - cbMultiline.addItemListener(defaultOptionListener); - - cbDotall = new JCheckBox(I18N.getMessage(I18N.getGUIBundle(), - "gui.dialog.parameter.regexp.regular_expression.regexp_options.dotall_mode")); - cbDotall.setToolTipText(I18N.getMessage(I18N.getGUIBundle(), - "gui.dialog.parameter.regexp.regular_expression.regexp_options.dotall_mode.tip")); - cbDotall.addItemListener(defaultOptionListener); - - cbUnicodeCase = new JCheckBox(I18N.getMessage(I18N.getGUIBundle(), - "gui.dialog.parameter.regexp.regular_expression.regexp_options.unicode_case")); - cbUnicodeCase.setToolTipText(I18N.getMessage(I18N.getGUIBundle(), - "gui.dialog.parameter.regexp.regular_expression.regexp_options.unicode_case.tip")); - cbUnicodeCase.addItemListener(defaultOptionListener); - - JPanel regexpOptionsPanelWrapper = new JPanel(new BorderLayout()); - JPanel regexpOptionsPanel = new JPanel(new GridBagLayout()); - regexpOptionsPanelWrapper.add(regexpOptionsPanel, BorderLayout.NORTH); - - c.insets = new Insets(12, 4, 0, 4); - c.gridx = 0; - c.gridy = 0; - c.weightx = 1; - c.weighty = 0; - c.fill = GridBagConstraints.HORIZONTAL; - regexpOptionsPanel.add(cbMultiline, c); - c.insets = new Insets(8, 4, 0, 4); - c.gridy = 1; - regexpOptionsPanel.add(cbCaseInsensitive, c); - c.gridy = 2; - regexpOptionsPanel.add(cbUnicodeCase, c); - c.gridy = 3; - regexpOptionsPanel.add(cbDotall, c); - - // create tabbed panel - c.insets = new Insets(8, 4, 4, 4); - c.gridx = 0; - c.gridy = 0; - c.weightx = 1; - c.weighty = 1; - c.gridwidth = GridBagConstraints.REMAINDER; - c.fill = GridBagConstraints.BOTH; - testExp = new JTabbedPane(); - JScrollPane spInline = new ExtendedJScrollPane(inlineSearchPanel); - spInline.setBorder(null); - testExp.add( - I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.regular_expression.inline_search.title"), - spInline); - JScrollPane spFindings = new ExtendedJScrollPane(regexpFindingsList); - spFindings.setBorder(null); - testExp.add(I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.regular_expression.result_list.title"), - spFindings); - testExp.add( - I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.regular_expression.regexp_options.title"), - regexpOptionsPanelWrapper); - testerPanel.add(testExp, c); - - JPanel groupPanel = new JPanel(new GridBagLayout()); - c.insets = new Insets(4, 4, 4, 4); - c.gridx = 0; - c.gridy = 0; - c.weightx = 1; - c.weighty = 0; - c.fill = GridBagConstraints.HORIZONTAL; - groupPanel.add(regexpPanel, c); - - c.insets = new Insets(4, 4, 4, 4); - c.gridx = 0; - c.gridy = 1; - c.weightx = 1; - c.weighty = 0; - c.fill = GridBagConstraints.HORIZONTAL; - groupPanel.add(replacementPanel, c); - - c.insets = new Insets(4, 4, 4, 4); - c.gridx = 0; - c.gridy = 2; - c.weightx = 1; - c.weighty = 1; - c.fill = GridBagConstraints.BOTH; - groupPanel.add(testerPanel, c); - - panel.add(groupPanel, 1, 0); - - if (supportsItems) { - // item shortcuts list - itemShortcutsList = new JList(items.toArray(new String[items.size()])); - itemShortcutsList - .setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.item_shortcuts.tip")); - itemShortcutsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - itemShortcutsList.addMouseListener(new MouseListener() { - - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { - String text = regexpTextField.getText(); - int cursorPosition = regexpTextField.getCaretPosition(); - int index = itemShortcutsList.getSelectedIndex(); - if (index > -1 && index < itemShortcutsList.getModel().getSize()) { - String insertionString = itemShortcutsList.getModel().getElementAt(index).toString(); - String newText = text.substring(0, cursorPosition) + insertionString - + (cursorPosition < text.length() ? text.substring(cursorPosition) : ""); - regexpTextField.setText(newText); - regexpTextField.setCaretPosition(cursorPosition + insertionString.length()); - regexpTextField.requestFocus(); - fireRegularExpressionUpdated(); - } - } - } - - @Override - public void mouseEntered(MouseEvent e) {} - - @Override - public void mouseExited(MouseEvent e) {} - - @Override - public void mousePressed(MouseEvent e) {} - - @Override - public void mouseReleased(MouseEvent e) {} - }); - JScrollPane itemShortcutsPane = new ExtendedJScrollPane(itemShortcutsList); - itemShortcutsPane.setBorder(createTitledBorder( - I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.item_shortcuts.border"))); - - // matched items list - matchedItemsListModel = new DefaultListModel(); - JList matchedItemsList = new JList(matchedItemsListModel); - matchedItemsList - .setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.matched_items.tip")); - // add custom cell renderer to disallow selections - matchedItemsList.setCellRenderer(new DefaultListCellRenderer() { - - private static final long serialVersionUID = -5795848004756768378L; - - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, - boolean cellHasFocus) { - return super.getListCellRendererComponent(list, value, index, false, false); - } - }); - JScrollPane matchedItemsPanel = new ExtendedJScrollPane(matchedItemsList); - matchedItemsPanel.setBorder(createTitledBorder( - I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.matched_items.border"))); - - // item panel on right side of dialog - JPanel itemPanel = new JPanel(createGridLayout(1, 2)); - itemPanel.add(itemShortcutsPane, 0, 0); - itemPanel.add(matchedItemsPanel, 0, 1); - - panel.add(itemPanel, 0, 1); - } - - okButton = makeOkButton("regexp_property_dialog_apply"); - fireRegularExpressionUpdated(); - - layoutDefault(panel, supportsItems ? NORMAL : NARROW, okButton, makeCancelButton()); - } - - private void updateRegexpOptions() { - boolean multiline = cbMultiline.isSelected(); - boolean caseInsensitive = cbCaseInsensitive.isSelected(); - boolean dotall = cbDotall.isSelected(); - boolean unicodeCase = cbUnicodeCase.isSelected(); - - String flags = getFlags(regexpTextField.getText()); - - if (multiline != flags.contains("m")) { - cbMultiline.setSelected(flags.contains("m")); - } - if (caseInsensitive != flags.contains("i")) { - cbCaseInsensitive.setSelected(flags.contains("i")); - } - if (dotall != flags.contains("s")) { - cbDotall.setSelected(flags.contains("s")); - } - if (unicodeCase != flags.contains("u")) { - cbUnicodeCase.setSelected(flags.contains("u")); - } - } - - private String getFlags(String pattern) { - if (!pattern.startsWith("(?")) { - return ""; - } - if (pattern.startsWith("(?-")) { - return ""; - } - if (pattern.indexOf(")") == -1) { - return ""; - } - String flags = pattern.substring(2, pattern.indexOf(")")); - return flags.split("-")[0]; - } - - private void fireRegexpOptionsChanged() { - boolean multiline = cbMultiline.isSelected(); - boolean caseInsensitive = cbCaseInsensitive.isSelected(); - boolean dotall = cbDotall.isSelected(); - boolean unicodeCase = cbUnicodeCase.isSelected(); - - String pattern = regexpTextField.getText(); - String flags = getFlags(pattern); - if (flags.contains("m")) { - if (!multiline) { - flags = flags.replace("m", ""); - } - } else { - if (multiline) { - flags += "m"; - } - } - - if (flags.contains("i")) { - if (!caseInsensitive) { - flags = flags.replace("i", ""); - } - } else { - if (caseInsensitive) { - flags += "i"; - } - } - - if (flags.contains("u")) { - if (!unicodeCase) { - flags = flags.replace("u", ""); - } - } else { - if (unicodeCase) { - flags += "u"; - } - } - - if (flags.contains("s")) { - if (!dotall) { - flags = flags.replace("s", ""); - } - } else { - if (dotall) { - flags += "s"; - } - } - - if (!flags.equals("") || pattern.startsWith("(?") && getFlags(pattern).equals("")) { - flags = "(?" + flags + ")"; - } - - if (pattern.startsWith("(?") && !pattern.startsWith("(?-")) { - int oldFlagsEnd = pattern.indexOf(")"); - if (oldFlagsEnd == -1) { - oldFlagsEnd = 1; - } - oldFlagsEnd++; - pattern = flags + pattern.substring(oldFlagsEnd); - } else { - pattern = flags + pattern; - } - int caretPosition = regexpTextField.getCaretPosition(); - regexpTextField.setText(pattern); - if (caretPosition < pattern.length()) { - regexpTextField.setCaretPosition(caretPosition); - } else { - regexpTextField.setCaretPosition(pattern.length()); - } - fireRegularExpressionUpdated(); - } - - private void fireRegularExpressionUpdated() { - boolean regularExpressionValid = false; - Pattern pattern = null; - try { - pattern = Pattern.compile(regexpTextField.getText()); - regularExpressionValid = true; - } catch (PatternSyntaxException e) { - regularExpressionValid = false; - } - if (supportsItems) { - matchedItemsListModel.clear(); - if (regularExpressionValid && pattern != null) { - for (String previewString : items) { - if (pattern.matcher(previewString).matches()) { - matchedItemsListModel.addElement(previewString); - } - } - } - } - if (regularExpressionValid) { - errorMessage.setText(NO_ERROR_MESSAGE); - errorMessage.setIcon(NO_ERROR_ICON); - okButton.setEnabled(true); - inlineSearchDocument.updatePattern(regexpTextField.getText()); - - } else { - errorMessage.setText(ERROR_MESSAGE); - errorMessage.setIcon(ERROR_ICON); - okButton.setEnabled(false); - inlineSearchDocument.clearResults(); - } - } - - private class InsertionAction extends LoggedAbstractAction { - - private static final long serialVersionUID = -5185173378762191200L; - private final String insertionString; - - public InsertionAction(String title, String insertionString) { - putValue(Action.NAME, title); - this.insertionString = insertionString; - } - - @Override - public void loggedActionPerformed(ActionEvent e) { - String text = regexpTextField.getText(); - - // is shortcut a construct? - boolean isConstruct = false; - int row = -1; - for (int i = 0; i < regexpConstructs.length; i++) { - if (regexpConstructs[i][0].equals(insertionString)) { - isConstruct = true; - row = i; - break; - } - } - if (isConstruct) { - if (regexpConstructInsertionEncloseSelected[row] && regexpTextField.getSelectedText() != null) { - int selectionStart = regexpTextField.getSelectionStart(); - int selectionEnd = regexpTextField.getSelectionEnd(); - String newText = text.substring(0, selectionStart) - + insertionString.substring(0, regexpConstructInsertionSelectionIndices[row][0]) - + text.substring(selectionStart, selectionEnd) + insertionString - .substring(regexpConstructInsertionSelectionIndices[row][0], insertionString.length()) - + text.substring(selectionEnd, text.length()); - regexpTextField.setText(newText); - regexpTextField.setCaretPosition(selectionEnd - regexpConstructInsertionCaretAdjustment[row]); - regexpTextField.setSelectionStart(selectionStart + regexpConstructInsertionSelectionIndices[row][0]); - regexpTextField.setSelectionEnd(selectionEnd + regexpConstructInsertionSelectionIndices[row][1]); - } else { - int cursorPosition = regexpTextField.getCaretPosition(); - String newText = text.substring(0, cursorPosition) + insertionString - + (cursorPosition < text.length() ? text.substring(cursorPosition) : ""); - regexpTextField.setText(newText); - regexpTextField.setCaretPosition( - cursorPosition + insertionString.length() + regexpConstructInsertionCaretAdjustment[row]); - regexpTextField.setSelectionStart(cursorPosition + regexpConstructInsertionSelectionIndices[row][0]); - regexpTextField.setSelectionEnd(cursorPosition + regexpConstructInsertionSelectionIndices[row][1]); - } - } else { - int cursorPosition = regexpTextField.getCaretPosition(); - String newText = text.substring(0, cursorPosition) + insertionString - + (cursorPosition < text.length() ? text.substring(cursorPosition) : ""); - regexpTextField.setText(newText); - regexpTextField.setCaretPosition(cursorPosition + insertionString.length()); - } - regexpTextField.requestFocus(); - fireRegularExpressionUpdated(); - } - } - - /** - * Sets the text of the search field. - * - * @param text - */ - public void setSearchFieldText(String text) { - try { - this.inlineSearchDocument.insertString(0, text, new SimpleAttributeSet()); - } catch (BadLocationException e) { - } - } - - public String getRegexp() { - return regexpTextField.getText(); - } - - @Override - protected String getInfoText() { - return infoText; - } -} +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. +*/ +package com.rapidminer.gui.properties; + +import static com.rapidminer.gui.search.GlobalSearchGUIUtilities.HTML_TAG_CLOSE; +import static com.rapidminer.gui.search.GlobalSearchGUIUtilities.HTML_TAG_OPEN; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ItemListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Collection; +import java.util.logging.Level; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import javax.swing.Action; +import javax.swing.BorderFactory; +import javax.swing.DefaultListCellRenderer; +import javax.swing.DefaultListModel; +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTabbedPane; +import javax.swing.JTextField; +import javax.swing.JTextPane; +import javax.swing.ListSelectionModel; +import javax.swing.SwingConstants; +import javax.swing.border.Border; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.DefaultStyledDocument; +import javax.swing.text.SimpleAttributeSet; +import javax.swing.text.Style; +import javax.swing.text.StyleConstants; + +import org.apache.commons.lang.ArrayUtils; + +import com.rapidminer.gui.ApplicationFrame; +import com.rapidminer.gui.LoggedAbstractAction; +import com.rapidminer.gui.tools.ExtendedJScrollPane; +import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.gui.tools.components.PlainArrowDropDownButton; +import com.rapidminer.gui.tools.dialogs.ButtonDialog; +import com.rapidminer.tools.I18N; +import com.rapidminer.tools.LogService; +import com.rapidminer.tools.Tools; + +import groovy.swing.impl.DefaultAction; + + +/** + * A dialog to create and edit regular expressions. Can be created with a given predefined regular + * expression (normally a previously set value). A collection of item strings can be given to the + * dialog which are then available as shortcuts. Additionally, a list shows which of these items + * match the regular expression. If the item collection is null, both lists will not be visible. + * + * The dialog shows an inline preview displaying where the given pattern matches. It also shows a + * list of matches, together with their matching groups. + * + * @author Tobias Malbrecht, Dominik Halfkann, Simon Fischer + */ +public class RegexpPropertyDialog extends ButtonDialog { + + private static final long serialVersionUID = 5396725165122306231L; + + private RegexpSearchStyledDocument inlineSearchDocument = null; + private RegexpReplaceStyledDocument inlineReplaceDocument = null; + + private JTabbedPane testExp = null; + + private DefaultListModel resultsListModel = new DefaultListModel<>(); + + private static String[][] regexpConstructs = { + { ".", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.any_character") }, + { "[]", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.bracket_expression") }, + { "[^]", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.not_bracket_expression") }, + { "()", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.capturing_group") }, + { "?", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.zero_one_quantifier") }, + { "*", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.zero_more_quantifier") }, + { "+", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.one_more_quantifier") }, + { "{n}", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.exact_quantifier") }, + { "{min,}", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.min_quantifier") }, + { "{min,max}", + I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.min_max_quantifier") }, + { "|", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.constructs.disjunction") }, }; + + // adjust the caret by this amount upon insertion + private static int[] regexpConstructInsertionCaretAdjustment = { 0, -1, -1, -1, 0, 0, 0, -1, -2, -1, -5, 0, }; + + // select these construct characters upon insertion + private static int[][] regexpConstructInsertionSelectionIndices = { { 1, 1 }, { 1, 1 }, { 2, 2 }, { 1, 1 }, { 1, 1 }, + { 1, 1 }, { 1, 1 }, { 1, 2 }, { 1, 4 }, { 1, 8 }, { 1, 1 }, }; + + // enclose selected by construct + private static boolean[] regexpConstructInsertionEncloseSelected = { false, true, true, true, false, false, false, false, + false, false, false, }; + + private static String[][] regexpShortcuts = { + { ".*", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.arbitrary") }, + { "[a-zA-Z]", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.letter") }, + { "[a-z]", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.lowercase_letter") }, + { "[A-Z]", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.uppercase_letter") }, + { "[0-9]", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.digit") }, + { "\\w", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.word") }, + { "\\W", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.non_word") }, + { "\\s", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.whitespace") }, + { "\\S", I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.non_whitespace") }, + { "[-!\"#$%&'()*+,./:;<=>?@\\[\\\\\\]_`{|}~]", + I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.shortcuts.punctuation") }, }; + + private static final String ERROR_MESSAGE = I18N.getMessage(I18N.getGUIBundle(), + "gui.dialog.parameter.regexp.error.label"); + + private static final Icon ERROR_ICON = SwingTools + .createIcon("16/" + I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.error.icon")); + + private static final String NO_ERROR_MESSAGE = I18N.getMessage(I18N.getGUIBundle(), + "gui.dialog.parameter.regexp.no_error.label"); + + private static final Icon NO_ERROR_ICON = SwingTools + .createIcon("16/" + I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.no_error.icon")); + + private String infoText; + + private final JTextField regexpTextField; + + private final JTextField replacementTextField; + + private JList itemShortcutsList; + + private DefaultListModel matchedItemsListModel; + + private final Collection items; + + private boolean supportsItems = false; + + private final JLabel errorMessage; + + private JButton okButton; + + private JCheckBox cbCaseInsensitive; + private JCheckBox cbMultiline; + private JCheckBox cbDotall; + private JCheckBox cbUnicodeCase; + + /** Class representing a single regexp search result. **/ + private class RegExpResult { + + private String match; + private String[] groups; + private int number; + private boolean empty = false; + + public RegExpResult(String match, String[] groups, int number) { + this.match = match; + this.groups = groups; + this.number = number; + } + + public RegExpResult() { + // empty result + empty = true; + } + + @Override + public String toString() { + StringBuilder output = new StringBuilder(); + if (!empty) { + output.append(HTML_TAG_OPEN + "").append( + // "Match "+number+": '"+match+"'" + + I18N.getMessage(I18N.getGUIBundle(), + "gui.dialog.parameter.regexp.regular_expression.result_list.match", number, + "'" + Tools.escapeHTML(match) + "'") + ).append(""); + if (groups.length > 0) { + output.append("
      "); + for (String group : groups) { + // output += "
    1. Group matches: '" + groups[i] +"'
    2. "; + output.append("
    3. ").append(I18N.getMessage(I18N.getGUIBundle(), + "gui.dialog.parameter.regexp.regular_expression.result_list.group_match", + "'" + Tools.escapeHTML(group) + "'")).append("
    4. "); + + } + output.append(""); + } + output.append(HTML_TAG_CLOSE); + } else { + output.append(HTML_TAG_OPEN + "").append(I18N.getMessage( + I18N.getGUIBundle(), "gui.dialog.parameter.regexp.regular_expression.result_list.empty")).append(""); + output.append(HTML_TAG_CLOSE); + } + return output.toString(); + } + } + + /** A StyledDocument providing a live regexp search **/ + private class RegexpSearchStyledDocument extends DefaultStyledDocument { + + private static final long serialVersionUID = 1L; + + private Matcher matcher = Pattern.compile("").matcher(""); + + private Style keyStyle; + private Style rootStyle; + + { + rootStyle = addStyle("root", null); + + keyStyle = addStyle("key", rootStyle); + StyleConstants.setBackground(keyStyle, Color.YELLOW); + } + + @Override + public void insertString(int offs, String str, AttributeSet a) throws BadLocationException { + super.insertString(offs, str, a); + checkDocument(); + } + + @Override + public void remove(int offs, int len) throws BadLocationException { + super.remove(offs, len); + checkDocument(); + } + + private void checkDocument() { + + setCharacterAttributes(0, getLength(), rootStyle, true); + try { + matcher.reset(getText(0, getLength())); + int count = 0; + resultsListModel.clear(); + while (matcher.find()) { + if (matcher.end() <= matcher.start()) { + continue; + } + setCharacterAttributes(matcher.start(), matcher.end() - matcher.start(), keyStyle, true); + + String[] groups = new String[matcher.groupCount()]; + for (int i = 1; i <= matcher.groupCount(); i++) { + groups[i - 1] = matcher.group(i); + } + resultsListModel.addElement(new RegExpResult( + this.getText(matcher.start(), matcher.end() - matcher.start()), groups, count + 1)); + count++; + } + + if (count == 0) { + // add empty element + resultsListModel.addElement(new RegExpResult()); + } + + testExp.setTitleAt(1, I18N.getMessage(I18N.getGUIBundle(), + "gui.dialog.parameter.regexp.regular_expression.result_list.title") + " (" + count + ")"); + String replacedText; + try { + replacedText = matcher.replaceAll(replacementTextField.getText()); + } catch (Exception e) { + replacedText = ""; + } + inlineReplaceDocument.setText(replacedText); + updateRegexpOptions(); + } catch (BadLocationException ex) { + LogService.getRoot().log(Level.WARNING, RegexpPropertyDialog.class.getName() + ".bad_location", ex); + } + } + + public void updatePattern(String pattern) { + this.matcher = Pattern.compile(pattern).matcher(""); + checkDocument(); + } + + public void clearResults() { + resultsListModel.clear(); + resultsListModel.addElement(new RegExpResult()); + testExp.setTitleAt(1, + I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.regular_expression.result_list.title") + + " (0)"); + setCharacterAttributes(0, getLength(), rootStyle, true); + } + + } + + /** A StyledDocument with an added setText() method for interting the replaced text **/ + private class RegexpReplaceStyledDocument extends DefaultStyledDocument { + + private static final long serialVersionUID = 1L; + + public RegexpReplaceStyledDocument() { + super(); + } + + public void setText(String text) { + try { + remove(0, getLength()); + insertString(0, text, null); + } catch (BadLocationException e) { + LogService.getRoot().log(Level.WARNING, RegexpPropertyDialog.class.getName() + ".bad_location", e); + } + } + } + + /** Same as {@link #RegexpPropertyDialog(Collection, String, String, String) RegexpPropertyDialog(items, predefinedRegexp, description, null)} */ + public RegexpPropertyDialog(final Collection items, String predefinedRegexp, String description) { + this(items, predefinedRegexp, description, null); + } + + /** + * Creates a new {@link RegexpPropertyDialog} with the given items, predefined regexp and description, + * as well as an optional key for a replacement parameter. If the replacement key is not {@code null}, + * the dialog indicates which parameter is associated with the replacement, and the actual replacement expression + * can be extracted using {@link #getReplacement()}. If no replacement key is provided, the dialog will indicate + * that te replacement is only used as a preview. + * + * @param items + * list of predefined regexps that can be selected from a dropdown + * @param predefinedRegexp + * the initial regex + * @param description + * the description of the associated parameter + * @param replacementKey + * the key of a parameter that takes care of the replacement; can be {@code null} + * @since 9.3 + */ + public RegexpPropertyDialog(final Collection items, String predefinedRegexp, String description, String replacementKey) { + super(ApplicationFrame.getApplicationFrame(), "parameter.regexp", ModalityType.APPLICATION_MODAL, new Object[0]); + this.items = items; + this.supportsItems = items != null; + this.infoText = HTML_TAG_OPEN + I18N.getMessage(I18N.getGUIBundle(), getKey() + ".title") + ":
      " + + description + HTML_TAG_CLOSE; + Dimension size = new Dimension(420, 500); + this.setMinimumSize(size); + this.setPreferredSize(size); + + JPanel panel = new JPanel(createGridLayout(1, supportsItems ? 2 : 1)); + + // create regexp text field + regexpTextField = new JTextField(predefinedRegexp); + regexpTextField + .setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.regular_expression.tip")); + regexpTextField.addKeyListener(new KeyAdapter() { + + @Override + public void keyReleased(KeyEvent e) { + fireRegularExpressionUpdated(); + } + + }); + regexpTextField.requestFocus(); + + // create replacement text field + replacementTextField = new JTextField(); + replacementTextField + .setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.replacement.tip")); + replacementTextField.addKeyListener(new KeyAdapter() { + + @Override + public void keyReleased(KeyEvent e) { + fireRegularExpressionUpdated(); + } + + }); + + // create inline search documents + inlineSearchDocument = new RegexpSearchStyledDocument(); + inlineReplaceDocument = new RegexpReplaceStyledDocument(); + + // create search results list + DefaultListCellRenderer resultCellRenderer = new DefaultListCellRenderer() { + + private static final long serialVersionUID = 1L; + + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, + boolean cellHasFocus) { + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + setBackground(list.getBackground()); + setForeground(list.getForeground()); + setBorder(getNoFocusBorder()); + return this; + } + + private Border getNoFocusBorder() { + return BorderFactory.createMatteBorder(0, 0, 1, 0, Color.gray); + } + }; + + JList regexpFindingsList = new JList<>(resultsListModel); + regexpFindingsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + regexpFindingsList.setLayoutOrientation(JList.VERTICAL); + regexpFindingsList.setCellRenderer(resultCellRenderer); + + // regexp panel on left side of dialog + JPanel regexpPanel = new JPanel(new GridBagLayout()); + regexpPanel.setBorder(createTitledBorder(I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.regular_expression.border"))); + GridBagConstraints c = new GridBagConstraints(); + c.insets = new Insets(4, 4, 4, 0); + c.gridx = 0; + c.gridy = 0; + c.weightx = 1; + c.fill = GridBagConstraints.BOTH; + regexpPanel.add(regexpTextField, c); + + // make shortcut button + final Action nullAction = new DefaultAction(); + PlainArrowDropDownButton autoWireDropDownButton = PlainArrowDropDownButton.makeDropDownButton(nullAction); + + for (String[] popupItem : (String[][]) ArrayUtils.addAll(regexpConstructs, regexpShortcuts)) { + String shortcut = popupItem[0].length() > 14 ? popupItem[0].substring(0, 14) + "..." : popupItem[0]; + autoWireDropDownButton + .add(new InsertionAction("
      " + + shortcut + "" + popupItem[1] + "
      ", popupItem[0])); + } + c.insets = new Insets(4, 0, 4, 0); + c.gridx = 1; + c.weightx = 0; + c.fill = GridBagConstraints.HORIZONTAL; + regexpPanel.add(autoWireDropDownButton.getDropDownArrowButton(), c); + + // make delete button + c.insets = new Insets(4, 0, 4, 4); + c.gridx = 2; + c.weightx = 0; + c.fill = GridBagConstraints.HORIZONTAL; + JButton clearRegexpTextFieldButton = new JButton(SwingTools.createIcon("16/delete.png")); + clearRegexpTextFieldButton.addActionListener(e -> { + regexpTextField.setText(""); + fireRegularExpressionUpdated(); + regexpTextField.requestFocusInWindow(); + }); + + regexpPanel.add(clearRegexpTextFieldButton, c); + + errorMessage = new JLabel(NO_ERROR_MESSAGE, NO_ERROR_ICON, SwingConstants.LEFT); + errorMessage.setFocusable(false); + c.insets = new Insets(4, 8, 4, 4); + c.gridx = 0; + c.gridy = 1; + c.weightx = 0; + c.weighty = 0; + c.gridwidth = GridBagConstraints.REMAINDER; + regexpPanel.add(errorMessage, c); + + // create replacement panel + JPanel replacementPanel = new JPanel(new GridBagLayout()); + String replacementBorderKey; + if (replacementKey == null) { + replacementBorderKey = "gui.dialog.parameter.regexp.replacement.border"; + } else { + replacementBorderKey = "gui.dialog.parameter.regexp.replacement_non_preview.border"; + replacementKey = replacementKey.replace('_', ' '); + } + replacementPanel.setBorder(createTitledBorder(I18N.getMessage(I18N.getGUIBundle(), replacementBorderKey, replacementKey))); + + JPanel testerPanel = new JPanel(new GridBagLayout()); + + c.insets = new Insets(4, 4, 4, 0); + c.gridx = 0; + c.gridy = 0; + c.weightx = 1; + c.fill = GridBagConstraints.HORIZONTAL; + replacementPanel.add(replacementTextField, c); + + // create inline search panel + JPanel inlineSearchPanel = new JPanel(new GridBagLayout()); + + c.insets = new Insets(8, 4, 4, 4); + c.gridx = 0; + c.gridy = 0; + c.weightx = 1; + c.weighty = 0; + c.fill = GridBagConstraints.HORIZONTAL; + inlineSearchPanel.add( + new JLabel(I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.inline_search.search")), c); + + c.insets = new Insets(0, 0, 0, 0); + c.gridx = 0; + c.gridy = 1; + c.weightx = 1; + c.weighty = 1; + c.fill = GridBagConstraints.BOTH; + inlineSearchPanel.add(new JScrollPane(new JTextPane(inlineSearchDocument)), c); + + c.insets = new Insets(8, 4, 4, 4); + c.gridx = 0; + c.gridy = 2; + c.weightx = 1; + c.weighty = 0; + c.fill = GridBagConstraints.HORIZONTAL; + inlineSearchPanel.add( + new JLabel(I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.inline_search.replaced")), c); + + c.insets = new Insets(0, 0, 0, 0); + c.gridx = 0; + c.gridy = 3; + c.weightx = 1; + c.weighty = 1; + c.fill = GridBagConstraints.BOTH; + JTextPane replaceTextPane = new JTextPane(inlineReplaceDocument); + replaceTextPane.setEditable(false); + JScrollPane scrollpane = new JScrollPane(replaceTextPane); + scrollpane.setBorder(null); + inlineSearchPanel.add(new JScrollPane(replaceTextPane), c); + + // create regexp options panel + ItemListener defaultOptionListener = e -> fireRegexpOptionsChanged(); + + cbCaseInsensitive = new JCheckBox(I18N.getMessage(I18N.getGUIBundle(), + "gui.dialog.parameter.regexp.regular_expression.regexp_options.case_insensitive")); + cbCaseInsensitive.setToolTipText(I18N.getMessage(I18N.getGUIBundle(), + "gui.dialog.parameter.regexp.regular_expression.regexp_options.case_insensitive.tip")); + cbCaseInsensitive.addItemListener(defaultOptionListener); + + cbMultiline = new JCheckBox(I18N.getMessage(I18N.getGUIBundle(), + "gui.dialog.parameter.regexp.regular_expression.regexp_options.multiline_mode")); + cbMultiline.setToolTipText(I18N.getMessage(I18N.getGUIBundle(), + "gui.dialog.parameter.regexp.regular_expression.regexp_options.multiline_mode.tip")); + cbMultiline.addItemListener(defaultOptionListener); + + cbDotall = new JCheckBox(I18N.getMessage(I18N.getGUIBundle(), + "gui.dialog.parameter.regexp.regular_expression.regexp_options.dotall_mode")); + cbDotall.setToolTipText(I18N.getMessage(I18N.getGUIBundle(), + "gui.dialog.parameter.regexp.regular_expression.regexp_options.dotall_mode.tip")); + cbDotall.addItemListener(defaultOptionListener); + + cbUnicodeCase = new JCheckBox(I18N.getMessage(I18N.getGUIBundle(), + "gui.dialog.parameter.regexp.regular_expression.regexp_options.unicode_case")); + cbUnicodeCase.setToolTipText(I18N.getMessage(I18N.getGUIBundle(), + "gui.dialog.parameter.regexp.regular_expression.regexp_options.unicode_case.tip")); + cbUnicodeCase.addItemListener(defaultOptionListener); + + JPanel regexpOptionsPanelWrapper = new JPanel(new BorderLayout()); + JPanel regexpOptionsPanel = new JPanel(new GridBagLayout()); + regexpOptionsPanelWrapper.add(regexpOptionsPanel, BorderLayout.NORTH); + + c.insets = new Insets(12, 4, 0, 4); + c.gridx = 0; + c.gridy = 0; + c.weightx = 1; + c.weighty = 0; + c.fill = GridBagConstraints.HORIZONTAL; + regexpOptionsPanel.add(cbMultiline, c); + c.insets = new Insets(8, 4, 0, 4); + c.gridy = 1; + regexpOptionsPanel.add(cbCaseInsensitive, c); + c.gridy = 2; + regexpOptionsPanel.add(cbUnicodeCase, c); + c.gridy = 3; + regexpOptionsPanel.add(cbDotall, c); + + // create tabbed panel + c.insets = new Insets(8, 4, 4, 4); + c.gridx = 0; + c.gridy = 0; + c.weightx = 1; + c.weighty = 1; + c.gridwidth = GridBagConstraints.REMAINDER; + c.fill = GridBagConstraints.BOTH; + testExp = new JTabbedPane(); + JScrollPane spInline = new ExtendedJScrollPane(inlineSearchPanel); + spInline.setBorder(null); + testExp.add( + I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.regular_expression.inline_search.title"), + spInline); + JScrollPane spFindings = new ExtendedJScrollPane(regexpFindingsList); + spFindings.setBorder(null); + testExp.add(I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.regular_expression.result_list.title"), + spFindings); + testExp.add( + I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.regular_expression.regexp_options.title"), + regexpOptionsPanelWrapper); + testerPanel.add(testExp, c); + + JPanel groupPanel = new JPanel(new GridBagLayout()); + c.insets = new Insets(4, 4, 4, 4); + c.gridx = 0; + c.gridy = 0; + c.weightx = 1; + c.weighty = 0; + c.fill = GridBagConstraints.HORIZONTAL; + groupPanel.add(regexpPanel, c); + + c.insets = new Insets(4, 4, 4, 4); + c.gridx = 0; + c.gridy = 1; + c.weightx = 1; + c.weighty = 0; + c.fill = GridBagConstraints.HORIZONTAL; + groupPanel.add(replacementPanel, c); + + c.insets = new Insets(4, 4, 4, 4); + c.gridx = 0; + c.gridy = 2; + c.weightx = 1; + c.weighty = 1; + c.fill = GridBagConstraints.BOTH; + groupPanel.add(testerPanel, c); + + panel.add(groupPanel, 1, 0); + + if (supportsItems) { + // item shortcuts list + itemShortcutsList = new JList<>(items.toArray(new String[0])); + itemShortcutsList + .setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.item_shortcuts.tip")); + itemShortcutsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + itemShortcutsList.addMouseListener(new MouseAdapter() { + + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + String text = regexpTextField.getText(); + int cursorPosition = regexpTextField.getCaretPosition(); + int index = itemShortcutsList.getSelectedIndex(); + if (index > -1 && index < itemShortcutsList.getModel().getSize()) { + String insertionString = itemShortcutsList.getModel().getElementAt(index); + String newText = text.substring(0, cursorPosition) + insertionString + + (cursorPosition < text.length() ? text.substring(cursorPosition) : ""); + regexpTextField.setText(newText); + regexpTextField.setCaretPosition(cursorPosition + insertionString.length()); + regexpTextField.requestFocus(); + fireRegularExpressionUpdated(); + } + } + } + }); + JScrollPane itemShortcutsPane = new ExtendedJScrollPane(itemShortcutsList); + itemShortcutsPane.setBorder(createTitledBorder( + I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.item_shortcuts.border"))); + + // matched items list + matchedItemsListModel = new DefaultListModel<>(); + JList matchedItemsList = new JList<>(matchedItemsListModel); + matchedItemsList + .setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.matched_items.tip")); + // add custom cell renderer to disallow selections + matchedItemsList.setCellRenderer(new DefaultListCellRenderer() { + + private static final long serialVersionUID = -5795848004756768378L; + + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, + boolean cellHasFocus) { + return super.getListCellRendererComponent(list, value, index, false, false); + } + }); + JScrollPane matchedItemsPanel = new ExtendedJScrollPane(matchedItemsList); + matchedItemsPanel.setBorder(createTitledBorder( + I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.parameter.regexp.matched_items.border"))); + + // item panel on right side of dialog + JPanel itemPanel = new JPanel(createGridLayout(1, 2)); + itemPanel.add(itemShortcutsPane, 0, 0); + itemPanel.add(matchedItemsPanel, 0, 1); + + panel.add(itemPanel, 0, 1); + } + + okButton = makeOkButton("regexp_property_dialog_apply"); + fireRegularExpressionUpdated(); + + layoutDefault(panel, supportsItems ? NORMAL : NARROW, okButton, makeCancelButton()); + } + + private void updateRegexpOptions() { + boolean multiline = cbMultiline.isSelected(); + boolean caseInsensitive = cbCaseInsensitive.isSelected(); + boolean dotall = cbDotall.isSelected(); + boolean unicodeCase = cbUnicodeCase.isSelected(); + + String flags = getFlags(regexpTextField.getText()); + + if (multiline != flags.contains("m")) { + cbMultiline.setSelected(flags.contains("m")); + } + if (caseInsensitive != flags.contains("i")) { + cbCaseInsensitive.setSelected(flags.contains("i")); + } + if (dotall != flags.contains("s")) { + cbDotall.setSelected(flags.contains("s")); + } + if (unicodeCase != flags.contains("u")) { + cbUnicodeCase.setSelected(flags.contains("u")); + } + } + + private String getFlags(String pattern) { + if (!pattern.startsWith("(?")) { + return ""; + } + if (pattern.startsWith("(?-")) { + return ""; + } + int closingBracket = pattern.indexOf(')'); + if (closingBracket == -1) { + return ""; + } + String flags = pattern.substring(2, closingBracket); + return flags.split("-")[0]; + } + + private void fireRegexpOptionsChanged() { + boolean multiline = cbMultiline.isSelected(); + boolean caseInsensitive = cbCaseInsensitive.isSelected(); + boolean dotall = cbDotall.isSelected(); + boolean unicodeCase = cbUnicodeCase.isSelected(); + + String pattern = regexpTextField.getText(); + String flags = getFlags(pattern); + if (flags.contains("m")) { + if (!multiline) { + flags = flags.replace("m", ""); + } + } else { + if (multiline) { + flags += "m"; + } + } + + if (flags.contains("i")) { + if (!caseInsensitive) { + flags = flags.replace("i", ""); + } + } else { + if (caseInsensitive) { + flags += "i"; + } + } + + if (flags.contains("u")) { + if (!unicodeCase) { + flags = flags.replace("u", ""); + } + } else { + if (unicodeCase) { + flags += "u"; + } + } + + if (flags.contains("s")) { + if (!dotall) { + flags = flags.replace("s", ""); + } + } else { + if (dotall) { + flags += "s"; + } + } + + if (!flags.isEmpty() || pattern.startsWith("(?") && getFlags(pattern).isEmpty()) { + flags = "(?" + flags + ")"; + } + + if (pattern.startsWith("(?") && !pattern.startsWith("(?-")) { + int oldFlagsEnd = pattern.indexOf(')'); + if (oldFlagsEnd == -1) { + oldFlagsEnd = 1; + } + oldFlagsEnd++; + pattern = flags + pattern.substring(oldFlagsEnd); + } else { + pattern = flags + pattern; + } + int caretPosition = regexpTextField.getCaretPosition(); + regexpTextField.setText(pattern); + if (caretPosition < pattern.length()) { + regexpTextField.setCaretPosition(caretPosition); + } else { + regexpTextField.setCaretPosition(pattern.length()); + } + fireRegularExpressionUpdated(); + } + + private void fireRegularExpressionUpdated() { + boolean regularExpressionValid; + Pattern pattern = null; + try { + pattern = Pattern.compile(regexpTextField.getText()); + regularExpressionValid = true; + } catch (PatternSyntaxException e) { + regularExpressionValid = false; + } + if (supportsItems) { + matchedItemsListModel.clear(); + if (regularExpressionValid) { + for (String previewString : items) { + if (pattern.matcher(previewString).matches()) { + matchedItemsListModel.addElement(previewString); + } + } + } + } + if (regularExpressionValid) { + errorMessage.setText(NO_ERROR_MESSAGE); + errorMessage.setIcon(NO_ERROR_ICON); + okButton.setEnabled(true); + inlineSearchDocument.updatePattern(regexpTextField.getText()); + + } else { + errorMessage.setText(ERROR_MESSAGE); + errorMessage.setIcon(ERROR_ICON); + okButton.setEnabled(false); + inlineSearchDocument.clearResults(); + } + } + + private class InsertionAction extends LoggedAbstractAction { + + private static final long serialVersionUID = -5185173378762191200L; + private final String insertionString; + + public InsertionAction(String title, String insertionString) { + putValue(Action.NAME, title); + this.insertionString = insertionString; + } + + @Override + public void loggedActionPerformed(ActionEvent e) { + String text = regexpTextField.getText(); + + // is shortcut a construct? + boolean isConstruct = false; + int row = -1; + for (int i = 0; i < regexpConstructs.length; i++) { + if (regexpConstructs[i][0].equals(insertionString)) { + isConstruct = true; + row = i; + break; + } + } + if (isConstruct) { + if (regexpConstructInsertionEncloseSelected[row] && regexpTextField.getSelectedText() != null) { + int selectionStart = regexpTextField.getSelectionStart(); + int selectionEnd = regexpTextField.getSelectionEnd(); + String newText = text.substring(0, selectionStart) + + insertionString.substring(0, regexpConstructInsertionSelectionIndices[row][0]) + + text.substring(selectionStart, selectionEnd) + insertionString + .substring(regexpConstructInsertionSelectionIndices[row][0], insertionString.length()) + + text.substring(selectionEnd, text.length()); + regexpTextField.setText(newText); + regexpTextField.setCaretPosition(selectionEnd - regexpConstructInsertionCaretAdjustment[row]); + regexpTextField.setSelectionStart(selectionStart + regexpConstructInsertionSelectionIndices[row][0]); + regexpTextField.setSelectionEnd(selectionEnd + regexpConstructInsertionSelectionIndices[row][1]); + } else { + int cursorPosition = regexpTextField.getCaretPosition(); + String newText = text.substring(0, cursorPosition) + insertionString + + (cursorPosition < text.length() ? text.substring(cursorPosition) : ""); + regexpTextField.setText(newText); + regexpTextField.setCaretPosition( + cursorPosition + insertionString.length() + regexpConstructInsertionCaretAdjustment[row]); + regexpTextField.setSelectionStart(cursorPosition + regexpConstructInsertionSelectionIndices[row][0]); + regexpTextField.setSelectionEnd(cursorPosition + regexpConstructInsertionSelectionIndices[row][1]); + } + } else { + int cursorPosition = regexpTextField.getCaretPosition(); + String newText = text.substring(0, cursorPosition) + insertionString + + (cursorPosition < text.length() ? text.substring(cursorPosition) : ""); + regexpTextField.setText(newText); + regexpTextField.setCaretPosition(cursorPosition + insertionString.length()); + } + regexpTextField.requestFocus(); + fireRegularExpressionUpdated(); + } + } + + /** + * Sets the text of the search field. + * + * @param text + */ + public void setSearchFieldText(String text) { + try { + this.inlineSearchDocument.insertString(0, text, new SimpleAttributeSet()); + } catch (BadLocationException e) { + } + } + + public String getRegexp() { + return regexpTextField.getText(); + } + + /** + * Get the content of the {@link #replacementTextField}. + * + * @since 9.3 + */ + public String getReplacement() { + return replacementTextField.getText(); + } + + /** + * Set the content of the {@link #replacementTextField}. + * + * @since 9.3 + */ + public void setReplacement(String replacement) { + replacementTextField.setText(replacement); + } + + @Override + protected String getInfoText() { + return infoText; + } +} diff --git a/src/main/java/com/rapidminer/gui/properties/SettingsDialog.java b/src/main/java/com/rapidminer/gui/properties/SettingsDialog.java index 5e479ba9b..d8565d186 100644 --- a/src/main/java/com/rapidminer/gui/properties/SettingsDialog.java +++ b/src/main/java/com/rapidminer/gui/properties/SettingsDialog.java @@ -20,7 +20,6 @@ import java.awt.BorderLayout; import java.awt.Dimension; -import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; @@ -50,8 +49,6 @@ import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; -import org.jdesktop.swingx.prompt.PromptSupport; - import com.rapidminer.gui.ApplicationFrame; import com.rapidminer.gui.tools.ResourceAction; import com.rapidminer.gui.tools.SwingTools; @@ -302,9 +299,8 @@ public void actionPerformed(final ActionEvent e) { filterNameField.requestFocusInWindow(); } }); - PromptSupport.setPrompt(I18N.getMessage(I18N.getGUIBundle(), "gui.label.settings.filter_field.prompt"), + SwingTools.setPrompt(I18N.getMessage(I18N.getGUIBundle(), "gui.label.settings.filter_field.prompt"), filterNameField); - PromptSupport.setFontStyle(Font.ITALIC, filterNameField); ResourceAction deleteFilterAction = new ResourceAction(true, "settings.filter_delete") { diff --git a/src/main/java/com/rapidminer/gui/properties/SettingsItems.java b/src/main/java/com/rapidminer/gui/properties/SettingsItems.java index 7649ae9ed..b5edef1da 100644 --- a/src/main/java/com/rapidminer/gui/properties/SettingsItems.java +++ b/src/main/java/com/rapidminer/gui/properties/SettingsItems.java @@ -24,6 +24,7 @@ import com.rapidminer.gui.properties.SettingsItem.Type; import com.rapidminer.parameter.ParameterHandler; +import com.rapidminer.parameter.ParameterType; import com.rapidminer.parameter.Parameters; import com.rapidminer.tools.ParameterService; import com.rapidminer.tools.Tools; @@ -90,8 +91,16 @@ public SettingsItem createAndAddItem(String key, Type type) { public void applyValues(ParameterHandler parameterHandler) { Parameters parameters = parameterHandler.getParameters(); Collection parameterKeys = ParameterService.getParameterKeys(); - parameters.getDefinedKeys().stream().filter(parameterKeys::contains) - .forEach(k -> ParameterService.setParameterValue(k, parameters.getParameterOrNull(k))); + for (String k : parameters.getDefinedKeys()) { + if (parameterKeys.contains(k)) { + ParameterType type = parameters.getParameterType(k); + String value = parameters.getParameterOrNull(k); + if (type != null && value != null) { + value = type.toString(value); + } + ParameterService.setParameterValue(k, value); + } + } } /** @see ParameterService#saveParameters() */ diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributeComboBox.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributeComboBox.java index cda2ce49e..8cf64ac7d 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributeComboBox.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributeComboBox.java @@ -18,7 +18,8 @@ */ package com.rapidminer.gui.properties.celleditors.value; -import java.util.Vector; +import java.util.ArrayList; +import java.util.List; import javax.swing.DefaultComboBoxModel; import javax.swing.JComboBox; import javax.swing.SwingUtilities; @@ -47,7 +48,7 @@ static class AttributeComboBoxModel extends DefaultComboBoxModel impleme private static final long serialVersionUID = 1L; private ParameterTypeAttribute attributeType; - private Vector> attributes = new Vector<>(); + private List> attributes = new ArrayList<>(); AttributeComboBoxModel(ParameterTypeAttribute attributeType) { this.attributeType = attributeType; @@ -99,7 +100,7 @@ public void informMetaDataChanged(MetaData newMetadata) { /** * @return the attribute <> value type pairs */ - Vector> getAttributePairs() { + List> getAttributePairs() { return attributes; } } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributeValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributeValueCellEditor.java index 86fa98c9a..6941092ca 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributeValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributeValueCellEditor.java @@ -77,7 +77,7 @@ public void setValue(Object x) { super.setValue(x); comboBox.setSelectedItem(value); if (value != null) { - textField.setText(value.toString()); + textField.setText(value); } else { textField.setText(""); } @@ -115,7 +115,6 @@ public void focusLost(FocusEvent e) { if (!e.isTemporary()) { comboBox.actionPerformed(new ActionEvent(comboBox, 12, COMBO_BOX_EDITED)); } - super.focusLost(e); } }); @@ -123,11 +122,9 @@ public void focusLost(FocusEvent e) { @Override public void keyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_ENTER) { - if (!comboBox.isPopupVisible()) { - comboBox.actionPerformed(new ActionEvent(comboBox, 12, COMBO_BOX_EDITED)); - e.consume(); - } + if (e.getKeyCode() == KeyEvent.VK_ENTER && !comboBox.isPopupVisible()) { + comboBox.actionPerformed(new ActionEvent(comboBox, 12, COMBO_BOX_EDITED)); + e.consume(); } } }); diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/ConnectionLocationValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/ConnectionLocationValueCellEditor.java new file mode 100644 index 000000000..5956f8d38 --- /dev/null +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/ConnectionLocationValueCellEditor.java @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.gui.properties.celleditors.value; + +import com.rapidminer.gui.actions.OpenAction; +import com.rapidminer.parameter.ParameterTypeConnectionLocation; +import com.rapidminer.repository.ConnectionEntry; +import com.rapidminer.repository.RepositoryLocation; + +/** + * Repository location cell editor that is specialized for {@link ConnectionEntry ConnectionEntries} and adds a second button + * that allows to open/edit the selected connection. + * + * @author Jan Czogalla + * @since 9.3 + */ +public class ConnectionLocationValueCellEditor extends RepositoryLocationWithExtraValueCellEditor { + + public ConnectionLocationValueCellEditor(ParameterTypeConnectionLocation type) { + super(type); + } + + @Override + protected String getExtraActionKey() { + return "connection.open_edit_connection"; + } + + @Override + protected void doExtraAction(RepositoryLocation repositoryLocation) { + OpenAction.open(repositoryLocation.getAbsoluteLocation(), true); + } + + @Override + protected Class getExpectedEntryClass() { + return ConnectionEntry.class; + } +} diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/ProcessLocationValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/ProcessLocationValueCellEditor.java index c3ddf3239..04e66dba2 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/ProcessLocationValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/ProcessLocationValueCellEditor.java @@ -18,159 +18,44 @@ */ package com.rapidminer.gui.properties.celleditors.value; -import java.awt.Component; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; - -import javax.swing.JButton; -import javax.swing.JPanel; -import javax.swing.JTable; -import javax.swing.SwingUtilities; -import javax.swing.event.CellEditorListener; -import javax.swing.event.ChangeEvent; - import com.rapidminer.RepositoryProcessLocation; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.actions.OpenAction; -import com.rapidminer.gui.tools.ProgressThread; -import com.rapidminer.gui.tools.ResourceAction; -import com.rapidminer.gui.tools.SwingTools; -import com.rapidminer.operator.UserError; import com.rapidminer.parameter.ParameterTypeProcessLocation; import com.rapidminer.repository.ProcessEntry; -import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; /** - * Cell editor that allows to select a repository entry by pressing a button. + * Repository location cell editor that is specialized for {@link ProcessEntry ProcessEntries} and adds a second button + * that allows to open the selected process. * - * @author Marcel Seifert, Nils Woehler + * @author Marcel Seifert, Nils Woehler, Jan Czogalla * */ -public class ProcessLocationValueCellEditor extends RepositoryLocationValueCellEditor { +public class ProcessLocationValueCellEditor extends RepositoryLocationWithExtraValueCellEditor { private static final long serialVersionUID = 1L; - private final JPanel surroundingPanel = new JPanel(new GridBagLayout()); - - private final JButton openProcessButton = new JButton(new ResourceAction(true, "execute_process.open_process") { - - private static final long serialVersionUID = 1L; - { - putValue(NAME, null); - } - - @Override - public void loggedActionPerformed(ActionEvent e) { - RepositoryLocation repositoryLocation; - RepositoryProcessLocation repositoryProcessLocation = null; - try { - if (getTextField() != null) { - repositoryLocation = RepositoryLocation.getRepositoryLocation(getTextFieldText(), getOperator()); - - if (repositoryLocation != null) { - repositoryProcessLocation = new RepositoryProcessLocation(repositoryLocation); - if (repositoryProcessLocation != null && RapidMinerGUI.getMainFrame().close()) { - OpenAction.open(repositoryProcessLocation, true); - } - } - } - } catch (UserError e1) { - SwingTools.showVerySimpleErrorMessage("malformed_repository_location", getTextField().getText()); - } - } - - }); - - private final CellEditorListener listener = new CellEditorListener() { - - @Override - public void editingStopped(ChangeEvent e) { - checkOpenProcessButtonEnabled(); - } - - @Override - public void editingCanceled(ChangeEvent e) { - // do nothing - } - }; - public ProcessLocationValueCellEditor(final ParameterTypeProcessLocation type) { super(type); - openProcessButton.setEnabled(false); - - GridBagConstraints gbc = new GridBagConstraints(); - - gbc.gridx = 0; - gbc.weightx = 1.0; - gbc.weighty = 1.0; - gbc.fill = GridBagConstraints.BOTH; - surroundingPanel.add(getPanel(), gbc); - - gbc.gridx += 1; - gbc.weightx = 0.0; - gbc.fill = GridBagConstraints.VERTICAL; - gbc.insets = new Insets(0, 2, 0, 0); - surroundingPanel.add(openProcessButton, gbc); - addCellEditorListener(listener); - } @Override - public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int col) { - // ensure text field is filled with correct values - super.getTableCellEditorComponent(table, value, isSelected, row, col); - checkOpenProcessButtonEnabled(); - return surroundingPanel; + protected String getExtraActionKey() { + return "execute_process.open_process"; } @Override - public void activate() { - if (openProcessButton.isEnabled()) { - openProcessButton.doClick(); - } else { - super.activate(); + protected void doExtraAction(RepositoryLocation repositoryLocation) { + if (RapidMinerGUI.getMainFrame().close()) { + RepositoryProcessLocation repositoryProcessLocation = new RepositoryProcessLocation(repositoryLocation); + OpenAction.open(repositoryProcessLocation, true); } } - /** - * Checks whether the provided repository location is valid and is a process. - */ - private void checkOpenProcessButtonEnabled() { - final String location = getTextFieldText(); - ProgressThread t = new ProgressThread("check_process_location_available", false, location) { - - @Override - public void run() { - boolean enabled = true; - try { - // check whether the process can be found - enabled = RepositoryLocation.getRepositoryLocation(location, getOperator()).locateEntry() instanceof ProcessEntry; - } catch (UserError | RepositoryException e) { - enabled = false; - } - final boolean enable = enabled; - SwingUtilities.invokeLater(new Runnable() { - - @Override - public void run() { - openProcessButton.setEnabled(enable); - } - }); - } - }; - t.setIndeterminate(true); - t.start(); - - } - - /** - * @return the text of the text field which cannot be null - */ - private String getTextFieldText() { - return getTextField().getText() != null ? getTextField().getText() : ""; + @Override + protected Class getExpectedEntryClass() { + return ProcessEntry.class; } } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/RegexpValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/RegexpValueCellEditor.java index 6c10128a7..b4ed46a55 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/RegexpValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/RegexpValueCellEditor.java @@ -23,10 +23,8 @@ import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; +import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; - import javax.swing.AbstractCellEditor; import javax.swing.JButton; import javax.swing.JPanel; @@ -37,6 +35,7 @@ import com.rapidminer.gui.tools.ResourceAction; import com.rapidminer.operator.Operator; import com.rapidminer.parameter.ParameterTypeRegexp; +import com.rapidminer.parameter.ParameterTypeString; /** @@ -54,19 +53,14 @@ public class RegexpValueCellEditor extends AbstractCellEditor implements Propert private final JTextField textField = new JTextField(12); private JButton button; + private Operator operator; - public RegexpValueCellEditor(final ParameterTypeRegexp type) { + public RegexpValueCellEditor(final ParameterTypeRegexp regexp) { panel.setLayout(new GridBagLayout()); - panel.setToolTipText(type.getDescription()); - textField.setToolTipText(type.getDescription()); - textField.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - fireEditingStopped(); - } - }); - textField.addFocusListener(new FocusListener() { + panel.setToolTipText(regexp.getDescription()); + textField.setToolTipText(regexp.getDescription()); + textField.addActionListener(e -> fireEditingStopped()); + textField.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { @@ -82,9 +76,6 @@ public void focusLost(FocusEvent e) { fireEditingStopped(); } } - - @Override - public void focusGained(FocusEvent e) {} }); GridBagConstraints c = new GridBagConstraints(); @@ -93,22 +84,37 @@ public void focusGained(FocusEvent e) {} c.weightx = 1; panel.add(textField, c); + ParameterTypeString replacement = regexp.getReplacementParameter(); + button = new JButton(new ResourceAction(true, "regexp") { private static final long serialVersionUID = 3989811306286704326L; @Override public void loggedActionPerformed(ActionEvent e) { - RegexpPropertyDialog dialog = new RegexpPropertyDialog(type.getPreviewList(), textField.getText(), - type.getDescription()); + String replacementKey = null; + if (replacement != null) { + replacementKey = replacement.getKey(); + } + RegexpPropertyDialog dialog = new RegexpPropertyDialog(regexp.getPreviewList(), textField.getText(), + regexp.getDescription(), replacementKey); + if (replacement != null && operator != null) { + String replacementValue = operator.getParameters().getParameterOrNull(replacementKey); + if (replacementValue != null) { + dialog.setReplacement(replacementValue); + } + } dialog.setVisible(true); if (dialog.wasConfirmed()) { textField.setText(dialog.getRegexp()); + if (replacement != null && operator != null) { + operator.setParameter(replacementKey, dialog.getReplacement()); + } } fireEditingStopped(); } }); - button.addFocusListener(new FocusListener() { + button.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { @@ -116,9 +122,6 @@ public void focusLost(FocusEvent e) { fireEditingStopped(); } } - - @Override - public void focusGained(FocusEvent e) {} }); button.setMargin(new Insets(0, 0, 0, 0)); c.weightx = 0; @@ -137,7 +140,9 @@ public boolean rendersLabel() { } @Override - public void setOperator(Operator operator) {} + public void setOperator(Operator operator) { + this.operator = operator; + } @Override public boolean useEditorAsRenderer() { diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/RemoteFileValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/RemoteFileValueCellEditor.java index 8c4ac2215..f3a74b24f 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/RemoteFileValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/RemoteFileValueCellEditor.java @@ -23,15 +23,14 @@ import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; - +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; import javax.swing.JButton; import javax.swing.JFileChooser; import javax.swing.JPanel; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.SwingUtilities; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.properties.DefaultRMCellEditor; @@ -73,24 +72,16 @@ public RemoteFileValueCellEditor(final ParameterTypeRemoteFile type) { gbc.weightx = 1; container.add(editorComponent, gbc); - ((JTextField) editorComponent).getDocument().addDocumentListener(new DocumentListener() { - - @Override - public void removeUpdate(DocumentEvent e) { - fireEditingStopped(); - - } - + editorComponent.addFocusListener(new FocusAdapter() { @Override - public void insertUpdate(DocumentEvent e) { - fireEditingStopped(); - - } - - @Override - public void changedUpdate(DocumentEvent e) { - fireEditingStopped(); - + public void focusLost(FocusEvent e) { + // The event is only fired if the focus loss is permanently, + // i.e. it is not fired if the user e.g. just switched to another window. + // Otherwise any changes made after switching back to RapidMiner would + // not be saved. + if (!e.isTemporary()) { + fireEditingStopped(); + } } }); @@ -134,17 +125,14 @@ public void run() { chooser.setFileSelectionMode(type.getFileSelectionMode()); getProgressListener().setCompleted(80); - SwingUtilities.invokeLater(new Runnable() { - - @Override - public void run() { - int returnvalue = chooser.showOpenDialog(RapidMinerGUI.getMainFrame()); - if (returnvalue == JFileChooser.APPROVE_OPTION) { - ((JTextField) editorComponent).setText(type.getRemoteFileSystemView() - .getNormalizedPathName(chooser.getSelectedFile())); - } - fileOpenButton.setEnabled(true); + SwingUtilities.invokeLater(() -> { + int returnvalue = chooser.showOpenDialog(RapidMinerGUI.getMainFrame()); + if (returnvalue == JFileChooser.APPROVE_OPTION) { + ((JTextField) editorComponent).setText(type.getRemoteFileSystemView() + .getNormalizedPathName(chooser.getSelectedFile())); + fireEditingStopped(); } + fileOpenButton.setEnabled(true); }); } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/RepositoryLocationValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/RepositoryLocationValueCellEditor.java index 21e398b9d..751564af1 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/RepositoryLocationValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/RepositoryLocationValueCellEditor.java @@ -23,10 +23,8 @@ import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; +import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; - import javax.swing.AbstractCellEditor; import javax.swing.JButton; import javax.swing.JPanel; @@ -63,13 +61,7 @@ public RepositoryLocationValueCellEditor(final ParameterTypeRepositoryLocation t panel.setLayout(gridBagLayout); panel.setToolTipText(type.getDescription()); textField.setToolTipText(type.getDescription()); - textField.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - fireEditingStopped(); - } - }); + textField.addActionListener(e -> fireEditingStopped()); GridBagConstraints c = new GridBagConstraints(); c.fill = GridBagConstraints.BOTH; @@ -106,7 +98,7 @@ public void loggedActionPerformed(ActionEvent e) { fireEditingStopped(); } }); - button.addFocusListener(new FocusListener() { + button.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { @@ -124,9 +116,6 @@ public void focusLost(FocusEvent e) { fireEditingStopped(); } } - - @Override - public void focusGained(FocusEvent e) {} }); button.setMargin(new Insets(0, 0, 0, 0)); c.gridwidth = GridBagConstraints.REMAINDER; @@ -134,7 +123,7 @@ public void focusGained(FocusEvent e) {} c.insets = new Insets(0, 5, 0, 0); panel.add(button, c); - textField.addFocusListener(new FocusListener() { + textField.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { @@ -152,9 +141,6 @@ public void focusLost(FocusEvent e) { fireEditingStopped(); } } - - @Override - public void focusGained(FocusEvent e) {} }); } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/RepositoryLocationWithExtraValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/RepositoryLocationWithExtraValueCellEditor.java new file mode 100644 index 000000000..81ffd4467 --- /dev/null +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/RepositoryLocationWithExtraValueCellEditor.java @@ -0,0 +1,177 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.gui.properties.celleditors.value; + +import java.awt.Component; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.JTable; +import javax.swing.SwingUtilities; +import javax.swing.event.CellEditorListener; +import javax.swing.event.ChangeEvent; + +import com.rapidminer.gui.tools.ProgressThread; +import com.rapidminer.gui.tools.ResourceAction; +import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.operator.UserError; +import com.rapidminer.parameter.ParameterTypeRepositoryLocation; +import com.rapidminer.repository.Entry; +import com.rapidminer.repository.RepositoryException; +import com.rapidminer.repository.RepositoryLocation; + +/** + * Abstract repository location cell editor that is specialized for a special {@link Entry} type and adds a second button + * that allows to interact with the selected entry if it is of the specified type. + * + * @see #getExtraActionKey() + * @see #doExtraAction(RepositoryLocation) + * @see #getExpectedEntryClass() + * + * @author Marcel Seifert, Nils Woehler, Jan Czogalla + * @since 9.3 + */ +public abstract class RepositoryLocationWithExtraValueCellEditor extends RepositoryLocationValueCellEditor { + private final JPanel surroundingPanel = new JPanel(new GridBagLayout()); + private final JButton extraButton = new JButton(new ResourceAction(true, getExtraActionKey()) { + + private static final long serialVersionUID = 1L; + + { + putValue(NAME, null); + } + + @Override + public void loggedActionPerformed(ActionEvent e) { + RepositoryLocation repositoryLocation; + try { + if (getTextField() != null) { + repositoryLocation = RepositoryLocation.getRepositoryLocation(getTextFieldText(), getOperator()); + if (repositoryLocation != null) { + doExtraAction(repositoryLocation); + } + } + } catch (UserError e1) { + SwingTools.showVerySimpleErrorMessage("malformed_repository_location", getTextField().getText()); + } + } + + }); + private final CellEditorListener listener = new CellEditorListener() { + + @Override + public void editingStopped(ChangeEvent e) { + checkExtraButtonEnabled(); + } + + @Override + public void editingCanceled(ChangeEvent e) { + // do nothing + } + }; + + public RepositoryLocationWithExtraValueCellEditor(ParameterTypeRepositoryLocation type) { + super(type); + extraButton.setEnabled(false); + + GridBagConstraints gbc = new GridBagConstraints(); + + gbc.gridx = 0; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + gbc.fill = GridBagConstraints.BOTH; + surroundingPanel.add(getPanel(), gbc); + + gbc.gridx += 1; + gbc.weightx = 0.0; + gbc.fill = GridBagConstraints.VERTICAL; + gbc.insets = new Insets(0, 2, 0, 0); + surroundingPanel.add(extraButton, gbc); + addCellEditorListener(listener); + + } + + @Override + public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int col) { + // ensure text field is filled with correct values + super.getTableCellEditorComponent(table, value, isSelected, row, col); + checkExtraButtonEnabled(); + return surroundingPanel; + } + + @Override + public void activate() { + if (extraButton.isEnabled()) { + extraButton.doClick(); + } else { + super.activate(); + } + } + + /** The i18n key for the extra action (i.e. second button). Only the icon will be used. */ + protected abstract String getExtraActionKey(); + + /** + * The action behind the second button. + * + * @param repositoryLocation + * the location to execute the action on; will never be called with {@code null} + */ + protected abstract void doExtraAction(RepositoryLocation repositoryLocation); + + /** Returns the expected (super) class of allowed entries. */ + protected abstract Class getExpectedEntryClass(); + + /** + * Checks whether the provided repository location is valid and is of the correct type. + * + * @see #getExpectedEntryClass() + */ + private void checkExtraButtonEnabled() { + final String location = getTextFieldText(); + ProgressThread t = new ProgressThread("check_process_location_available", false, location) { + + @Override + public void run() { + boolean enabled = true; + try { + // check whether the lcoation can be found and is of correct type + enabled = getExpectedEntryClass().isInstance(RepositoryLocation.getRepositoryLocation(location, getOperator()).locateEntry()); + } catch (UserError | RepositoryException e) { + enabled = false; + } + final boolean enable = enabled; + SwingUtilities.invokeLater(() -> extraButton.setEnabled(enable)); + } + }; + t.setIndeterminate(true); + t.start(); + + } + + /** + * @return the text of the text field which cannot be null + */ + private String getTextFieldText() { + return getTextField().getText() != null ? getTextField().getText() : ""; + } +} diff --git a/src/main/java/com/rapidminer/gui/properties/tablepanel/cells/implementations/CellTypeDateImpl.java b/src/main/java/com/rapidminer/gui/properties/tablepanel/cells/implementations/CellTypeDateImpl.java index eec6bf362..6e9bc284d 100644 --- a/src/main/java/com/rapidminer/gui/properties/tablepanel/cells/implementations/CellTypeDateImpl.java +++ b/src/main/java/com/rapidminer/gui/properties/tablepanel/cells/implementations/CellTypeDateImpl.java @@ -19,9 +19,7 @@ package com.rapidminer.gui.properties.tablepanel.cells.implementations; import java.awt.BorderLayout; -import java.awt.Color; import java.awt.Dimension; -import java.awt.Font; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -31,16 +29,12 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; - import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JFormattedTextField; import javax.swing.JPanel; import javax.swing.KeyStroke; -import org.jdesktop.swingx.prompt.PromptSupport; -import org.jdesktop.swingx.prompt.PromptSupport.FocusBehavior; - import com.michaelbaranov.microba.calendar.DatePicker; import com.rapidminer.example.set.CustomFilter.CustomFilters; import com.rapidminer.gui.properties.tablepanel.TablePanel; @@ -48,6 +42,7 @@ import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellTypeDate; import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellTypeDateTime; import com.rapidminer.gui.properties.tablepanel.model.TablePanelModel; +import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.tools.I18N; @@ -179,10 +174,7 @@ public void actionPerformed(final ActionEvent e) { // set syntax assist if available String syntaxHelp = model.getSyntaxHelpAt(rowIndex, columnIndex); if (syntaxHelp != null && !"".equals(syntaxHelp.trim())) { - PromptSupport.setForeground(Color.LIGHT_GRAY, field); - PromptSupport.setPrompt(syntaxHelp, field); - PromptSupport.setFontStyle(Font.ITALIC, field); - PromptSupport.setFocusBehavior(FocusBehavior.SHOW_PROMPT, field); + SwingTools.setPrompt(syntaxHelp, field); } // misc settings diff --git a/src/main/java/com/rapidminer/gui/properties/tablepanel/cells/implementations/CellTypeTextFieldDefaultImpl.java b/src/main/java/com/rapidminer/gui/properties/tablepanel/cells/implementations/CellTypeTextFieldDefaultImpl.java index b9dfd2178..10c282be5 100644 --- a/src/main/java/com/rapidminer/gui/properties/tablepanel/cells/implementations/CellTypeTextFieldDefaultImpl.java +++ b/src/main/java/com/rapidminer/gui/properties/tablepanel/cells/implementations/CellTypeTextFieldDefaultImpl.java @@ -18,21 +18,8 @@ */ package com.rapidminer.gui.properties.tablepanel.cells.implementations; -import com.rapidminer.gui.properties.tablepanel.TablePanel; -import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellType; -import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellTypeTextFieldDefault; -import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellTypeTextFieldInteger; -import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellTypeTextFieldNumerical; -import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellTypeTextFieldTime; -import com.rapidminer.gui.properties.tablepanel.model.TablePanelModel; -import com.rapidminer.gui.tools.SwingTools; -import com.rapidminer.tools.I18N; - import java.awt.BorderLayout; -import java.awt.Color; import java.awt.Dimension; -import java.awt.Font; - import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JFormattedTextField; @@ -40,8 +27,15 @@ import javax.swing.JPanel; import javax.swing.text.DefaultFormatterFactory; -import org.jdesktop.swingx.prompt.PromptSupport; -import org.jdesktop.swingx.prompt.PromptSupport.FocusBehavior; +import com.rapidminer.gui.properties.tablepanel.TablePanel; +import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellType; +import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellTypeTextFieldDefault; +import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellTypeTextFieldInteger; +import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellTypeTextFieldNumerical; +import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellTypeTextFieldTime; +import com.rapidminer.gui.properties.tablepanel.model.TablePanelModel; +import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.tools.I18N; /** @@ -89,10 +83,7 @@ public CellTypeTextFieldDefaultImpl(final TablePanelModel model, final int rowIn // set syntax assist if available String syntaxHelp = model.getSyntaxHelpAt(rowIndex, columnIndex); if (syntaxHelp != null && !"".equals(syntaxHelp.trim())) { - PromptSupport.setForeground(Color.LIGHT_GRAY, field); - PromptSupport.setPrompt(syntaxHelp, field); - PromptSupport.setFontStyle(Font.ITALIC, field); - PromptSupport.setFocusBehavior(FocusBehavior.SHOW_PROMPT, field); + SwingTools.setPrompt(syntaxHelp, field); } // see if content assist is possible for this field, if so add it diff --git a/src/main/java/com/rapidminer/gui/renderer/AbstractDataTablePlotterRenderer.java b/src/main/java/com/rapidminer/gui/renderer/AbstractDataTablePlotterRenderer.java index 9213ba1f3..cf95996d5 100644 --- a/src/main/java/com/rapidminer/gui/renderer/AbstractDataTablePlotterRenderer.java +++ b/src/main/java/com/rapidminer/gui/renderer/AbstractDataTablePlotterRenderer.java @@ -46,7 +46,9 @@ * given {@link DataTable}. * * @author Ingo Mierswa + * @deprecated since 9.2.1 */ +@Deprecated public abstract class AbstractDataTablePlotterRenderer extends AbstractRenderer { public static final String PARAMETER_PLOTTER = "plotter"; diff --git a/src/main/java/com/rapidminer/gui/renderer/RendererService.java b/src/main/java/com/rapidminer/gui/renderer/RendererService.java index 329215215..23c26df4d 100644 --- a/src/main/java/com/rapidminer/gui/renderer/RendererService.java +++ b/src/main/java/com/rapidminer/gui/renderer/RendererService.java @@ -43,6 +43,9 @@ import com.rapidminer.gui.MainFrame; import com.rapidminer.gui.new_plotter.integration.ExpertDataTableRenderer; import com.rapidminer.gui.renderer.data.ExampleSetPlotRenderer; +import com.rapidminer.gui.renderer.math.NumericalMatrixPlotRenderer; +import com.rapidminer.gui.renderer.models.KernelModelPlotRenderer; +import com.rapidminer.gui.renderer.weights.AttributeWeightsPlotRenderer; import com.rapidminer.gui.tools.IconSize; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.io.process.XMLTools; @@ -89,6 +92,12 @@ public String getIconName() { private static final String CORE_IOOBJECTS_XML = "ioobjects.xml"; + /** + * Contains all simple renderers that have been migrated to the new HTML5 Visualizations. Can be removed once the "show legacy simple charts" setting is removed. + */ + private static final Class[] MIGRATED_SIMPLE_RENDERER_CLASSES = new Class[] {ExampleSetPlotRenderer.class, AttributeWeightsPlotRenderer.class, + NumericalMatrixPlotRenderer.class, KernelModelPlotRenderer.class}; + private static final IconData ICON_DEFAULT_16 = new IconData("data.png", SwingTools.createIcon("16/data.png")); private static final IconData ICON_DEFAULT_24 = new IconData("data.png", SwingTools.createIcon("24/data.png")); private static final IconData ICON_DEFAULT_48 = new IconData("data.png", SwingTools.createIcon("48/data.png")); @@ -388,7 +397,7 @@ public static List getRenderersExcludingLegacyRenderers(String reporta boolean showLegacyAdvancedCharts = Boolean.parseBoolean(ParameterService.getParameterValue(MainFrame.PROPERTY_RAPIDMINER_GUI_PLOTTER_SHOW_LEGACY_ADVANCED_CHARTS)); // filter old charts and old advanced charts unless user has activated them in settings return renderers.stream().filter(renderer -> { - if (renderer.getClass().isAssignableFrom(ExampleSetPlotRenderer.class)) { + if (isMigratedSimpleRendererClass(renderer.getClass())) { return showLegacySimpleCharts; } else if (renderer.getClass().isAssignableFrom(ExpertDataTableRenderer.class)) { return showLegacyAdvancedCharts; @@ -537,4 +546,21 @@ private static IconData updateIconData(Class objectClass, Ma } return icon; } + + /** + * Checks if the given simple renderer class was already migrated to the new HTML5 visualizations. + * + * @param rendererClass + * the renderer class in question, never {@code null} + * @return {@code true} if the given renderer was migrated; {@code false} otherwise + */ + private static boolean isMigratedSimpleRendererClass(Class rendererClass) { + for (Class migratedClass : MIGRATED_SIMPLE_RENDERER_CLASSES) { + if (rendererClass.isAssignableFrom(migratedClass)) { + return true; + } + } + + return false; + } } diff --git a/src/main/java/com/rapidminer/gui/renderer/data/ExampleSetPlotRenderer.java b/src/main/java/com/rapidminer/gui/renderer/data/ExampleSetPlotRenderer.java index 7a70c3c61..03ec26514 100644 --- a/src/main/java/com/rapidminer/gui/renderer/data/ExampleSetPlotRenderer.java +++ b/src/main/java/com/rapidminer/gui/renderer/data/ExampleSetPlotRenderer.java @@ -36,7 +36,9 @@ * A renderer for the plot view of example sets. * * @author Ingo Mierswa + * @deprecated since 9.2.0 */ +@Deprecated public class ExampleSetPlotRenderer extends AbstractDataTablePlotterRenderer { @Override diff --git a/src/main/java/com/rapidminer/gui/renderer/math/NumericalMatrixPlotRenderer.java b/src/main/java/com/rapidminer/gui/renderer/math/NumericalMatrixPlotRenderer.java index 5892b9f7e..21701085e 100644 --- a/src/main/java/com/rapidminer/gui/renderer/math/NumericalMatrixPlotRenderer.java +++ b/src/main/java/com/rapidminer/gui/renderer/math/NumericalMatrixPlotRenderer.java @@ -29,7 +29,9 @@ /** * * @author Sebastian Land + * @deprecated since 9.2.1 */ +@Deprecated public class NumericalMatrixPlotRenderer extends AbstractDataTablePlotterRenderer { @Override diff --git a/src/main/java/com/rapidminer/gui/renderer/math/RainflowMatrixPlotRenderer.java b/src/main/java/com/rapidminer/gui/renderer/math/RainflowMatrixPlotRenderer.java index 0e5d2c124..174203c98 100644 --- a/src/main/java/com/rapidminer/gui/renderer/math/RainflowMatrixPlotRenderer.java +++ b/src/main/java/com/rapidminer/gui/renderer/math/RainflowMatrixPlotRenderer.java @@ -30,7 +30,9 @@ /** * * @author Sebastian Land + * @deprecated since 9.2.1 */ +@Deprecated public class RainflowMatrixPlotRenderer extends AbstractDataTablePlotterRenderer { @Override diff --git a/src/main/java/com/rapidminer/gui/renderer/models/KernelModelPlotRenderer.java b/src/main/java/com/rapidminer/gui/renderer/models/KernelModelPlotRenderer.java index 22d8719e9..903cb80b8 100644 --- a/src/main/java/com/rapidminer/gui/renderer/models/KernelModelPlotRenderer.java +++ b/src/main/java/com/rapidminer/gui/renderer/models/KernelModelPlotRenderer.java @@ -29,7 +29,9 @@ * A renderer for the plot view of kernel models. * * @author Ingo Mierswa + * @deprecated since 9.2.1 */ +@Deprecated public class KernelModelPlotRenderer extends AbstractDataTablePlotterRenderer { @Override diff --git a/src/main/java/com/rapidminer/gui/renderer/models/LocalPolynomialRegressionModelPlotRenderer.java b/src/main/java/com/rapidminer/gui/renderer/models/LocalPolynomialRegressionModelPlotRenderer.java index 4e4483351..48dcaf46d 100644 --- a/src/main/java/com/rapidminer/gui/renderer/models/LocalPolynomialRegressionModelPlotRenderer.java +++ b/src/main/java/com/rapidminer/gui/renderer/models/LocalPolynomialRegressionModelPlotRenderer.java @@ -27,8 +27,9 @@ /** * @author Sebastian Land - * + * @deprecated since 9.2.1 as it was not used anywhere */ +@Deprecated public class LocalPolynomialRegressionModelPlotRenderer extends AbstractDataTablePlotterRenderer { @Override diff --git a/src/main/java/com/rapidminer/gui/renderer/weights/AttributeWeightsPlotRenderer.java b/src/main/java/com/rapidminer/gui/renderer/weights/AttributeWeightsPlotRenderer.java index 6eadb3c79..340c68975 100644 --- a/src/main/java/com/rapidminer/gui/renderer/weights/AttributeWeightsPlotRenderer.java +++ b/src/main/java/com/rapidminer/gui/renderer/weights/AttributeWeightsPlotRenderer.java @@ -32,7 +32,9 @@ * A renderer for the plot view of attribute weights. * * @author Ingo Mierswa + * @deprecated since 9.2.1 */ +@Deprecated public class AttributeWeightsPlotRenderer extends AbstractDataTablePlotterRenderer { @Override diff --git a/src/main/java/com/rapidminer/gui/search/GlobalSearchController.java b/src/main/java/com/rapidminer/gui/search/GlobalSearchController.java index 266ab9985..5d9cb696b 100644 --- a/src/main/java/com/rapidminer/gui/search/GlobalSearchController.java +++ b/src/main/java/com/rapidminer/gui/search/GlobalSearchController.java @@ -27,6 +27,7 @@ import org.apache.lucene.search.ScoreDoc; import com.rapidminer.gui.search.model.GlobalSearchModel; +import com.rapidminer.gui.tools.MultiSwingWorker; import com.rapidminer.search.GlobalSearchCategory; import com.rapidminer.search.GlobalSearchRegistry; import com.rapidminer.search.GlobalSearchResult; @@ -176,7 +177,7 @@ protected GlobalSearchModel getModel() { */ private void searchOrAppendForCategory(final ScoreDoc offset, final String query, final String categoryId, int resultNumberLimit) { final int searchCountSnapshot = searchCounter.get(); - SwingWorker worker = new SwingWorker() { + MultiSwingWorker worker = new MultiSwingWorker() { @Override protected GlobalSearchResult doInBackground() throws Exception { @@ -235,7 +236,7 @@ private void logSearch(GlobalSearchResult result) { } } }; - worker.execute(); + worker.start(); } /** diff --git a/src/main/java/com/rapidminer/gui/search/GlobalSearchDialog.java b/src/main/java/com/rapidminer/gui/search/GlobalSearchDialog.java index a1a48f33e..1e2714737 100644 --- a/src/main/java/com/rapidminer/gui/search/GlobalSearchDialog.java +++ b/src/main/java/com/rapidminer/gui/search/GlobalSearchDialog.java @@ -480,6 +480,10 @@ private void displayNoResults() { * @return {@code true} if the UI was successfully added; {@code false} otherwise */ private boolean addGUIForCategory(final GlobalSearchCategory category) { + // Check if category is visible + if (!category.isVisible()) { + return false; + } String categoryId = category.getCategoryId(); try { GlobalSearchCategoryPanel catPanel = new GlobalSearchCategoryPanel(category, controller); diff --git a/src/main/java/com/rapidminer/gui/search/GlobalSearchGUIUtilities.java b/src/main/java/com/rapidminer/gui/search/GlobalSearchGUIUtilities.java index 119dae0ae..8ffb89a92 100644 --- a/src/main/java/com/rapidminer/gui/search/GlobalSearchGUIUtilities.java +++ b/src/main/java/com/rapidminer/gui/search/GlobalSearchGUIUtilities.java @@ -43,6 +43,8 @@ public enum GlobalSearchGUIUtilities { /** the max height of a GUI component that the Global Search GUI will accept */ public static final int MAX_HEIGHT = 50; + public static final String HTML_TAG_OPEN = ""; + public static final String HTML_TAG_CLOSE = ""; private static final String CATEGORY_ID_ACTIONS = "actions"; private static final String CATEGORY_ID_OPERATOR = "operator"; @@ -59,10 +61,6 @@ public enum GlobalSearchGUIUtilities { PREDEFINED_CATEGORY_ORDER.add(CATEGORY_ID_REPOSITORY); } - private static final String HTML_TAG_OPEN = ""; - private static final String HTML_TAG_CLOSE = ""; - - /** * Adds highlights to strings for search result hits. * diff --git a/src/main/java/com/rapidminer/gui/security/BlacklistedOperatorProcessEditor.java b/src/main/java/com/rapidminer/gui/security/BlacklistedOperatorProcessEditor.java index 441cfcb2d..3d04fdb50 100644 --- a/src/main/java/com/rapidminer/gui/security/BlacklistedOperatorProcessEditor.java +++ b/src/main/java/com/rapidminer/gui/security/BlacklistedOperatorProcessEditor.java @@ -37,12 +37,12 @@ import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; -import javax.swing.SwingWorker; import com.rapidminer.Process; import com.rapidminer.gui.MainFrame; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.processeditor.ExtendedProcessEditor; +import com.rapidminer.gui.tools.MultiSwingWorker; import com.rapidminer.gui.tools.NotificationPopup; import com.rapidminer.gui.tools.ResourceAction; import com.rapidminer.gui.tools.SwingTools; @@ -95,7 +95,7 @@ private void checkConstraint(Process process) { if (!OperatorService.hasBlacklistedOperators() || !RapidMinerGUI.getMainFrame().getProcessPanel().isShowing()) { return; } - new SwingWorker() { + new MultiSwingWorker() { @Override protected Void doInBackground() { @@ -125,7 +125,7 @@ private boolean isBlacklisted(Operator operator) { protected void process(List chunks) { chunks.forEach(BlacklistedOperatorProcessEditor.this::showNotification); } - }.execute(); + }.start(); } diff --git a/src/main/java/com/rapidminer/gui/tools/FilterTextField.java b/src/main/java/com/rapidminer/gui/tools/FilterTextField.java index ed5f09669..145d1b8ba 100644 --- a/src/main/java/com/rapidminer/gui/tools/FilterTextField.java +++ b/src/main/java/com/rapidminer/gui/tools/FilterTextField.java @@ -18,8 +18,6 @@ */ package com.rapidminer.gui.tools; -import java.awt.Color; -import java.awt.Font; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; @@ -29,9 +27,6 @@ import javax.swing.JTextField; import javax.swing.text.Document; -import org.jdesktop.swingx.prompt.PromptSupport; -import org.jdesktop.swingx.prompt.PromptSupport.FocusBehavior; - import com.rapidminer.tools.I18N; @@ -47,8 +42,8 @@ public class FilterTextField extends JTextField { private String defaultFilterText = I18N.getMessage(I18N.getGUIBundle(), "gui.filter_text_field.label"); - /** the value of the field when the last update was fired */ - private String valueLastUpdate = null; + /** the value of the field when the last update was fired, init with default getText() result */ + private String valueLastUpdate = ""; private final Collection filterListeners; @@ -77,10 +72,7 @@ public FilterTextField(Document doc, String text, int columns) { if (text != null && text.length() != 0) { setDefaultFilterText(text); } - PromptSupport.setForeground(Color.LIGHT_GRAY, this); - PromptSupport.setPrompt(defaultFilterText, this); - PromptSupport.setFontStyle(Font.ITALIC, this); - PromptSupport.setFocusBehavior(FocusBehavior.SHOW_PROMPT, this); + SwingTools.setPrompt(defaultFilterText, this); addKeyListener(new KeyListener() { @Override @@ -179,7 +171,7 @@ private void updateFilter(KeyEvent e) { public void setDefaultFilterText(String text) { this.defaultFilterText = text; - PromptSupport.setPrompt(text, this); + SwingTools.setPrompt(text, this); } public void setFilterText(String filterText) { diff --git a/src/main/java/com/rapidminer/gui/tools/FilterableListModel.java b/src/main/java/com/rapidminer/gui/tools/FilterableListModel.java index 655424ea9..43ff281d9 100644 --- a/src/main/java/com/rapidminer/gui/tools/FilterableListModel.java +++ b/src/main/java/com/rapidminer/gui/tools/FilterableListModel.java @@ -18,12 +18,15 @@ */ package com.rapidminer.gui.tools; +import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.LinkedList; +import java.util.List; import javax.swing.AbstractListModel; import com.rapidminer.external.alphanum.AlphanumComparator; +import com.rapidminer.external.alphanum.AlphanumComparator.AlphanumCaseSensitivity; /** @@ -35,22 +38,26 @@ */ public class FilterableListModel extends AbstractListModel implements FilterListener { - public abstract static class FilterCondition { + public interface FilterCondition { + + boolean matches(Object o); - public abstract boolean matches(Object o); } + /** @since 9.2.1 */ + public static final AlphanumComparator STRING_COMPARATOR = new AlphanumComparator(AlphanumCaseSensitivity.INSENSITIVE); + private static final long serialVersionUID = 552254394780900171L; - private LinkedList list; + private List list; - private LinkedList filteredList; + private List filteredList; private Comparator comparator; private String filterValue; - private LinkedList conditions = new LinkedList<>(); + private List conditions = new LinkedList<>(); public FilterableListModel() { this(true); @@ -64,10 +71,25 @@ public FilterableListModel() { * @since 9.2.0 */ public FilterableListModel(boolean sort) { - list = new LinkedList<>(); - filteredList = new LinkedList<>(); + this(new ArrayList<>(), sort); + + } + + /** + * Can sort if desired and starts out with the specified elements. + * + * @param elements + * the elements present in the model; assumed to be already sorted if {@code sorted} is {@code true} + * @param sort + * if {@code true}, will sort alpha-numerically; if {@code false} will not sort at all + * @since 9.2.1 + */ + @SuppressWarnings("unchecked") + public FilterableListModel(List elements, boolean sort) { + list = new ArrayList<>(elements); + filteredList = new ArrayList<>(elements); if (sort) { - comparator = Comparator.comparing(Object::toString, new AlphanumComparator(AlphanumComparator.AlphanumCaseSensitivity.INSENSITIVE)); + comparator = Comparator.comparing(Object::toString, STRING_COMPARATOR); } } @@ -82,10 +104,8 @@ public void valueChanged(String value) { } } else { for (E e : list) { - if (e.toString().toLowerCase().contains(value.toLowerCase())) { - if (!filteredByCondition(e)) { - filteredList.add(e); - } + if (e.toString().toLowerCase().contains(value.toLowerCase()) && !filteredByCondition(e)) { + filteredList.add(e); } } } @@ -188,10 +208,8 @@ private void filterConditionsChanged() { } } else { for (E e : list) { - if (e.toString().contains(filterValue)) { - if (!filteredByCondition(e)) { - filteredList.add(e); - } + if (e.toString().contains(filterValue) && !filteredByCondition(e)) { + filteredList.add(e); } } } diff --git a/src/main/java/com/rapidminer/gui/tools/IconSize.java b/src/main/java/com/rapidminer/gui/tools/IconSize.java index d241bcd5c..939c75a2c 100644 --- a/src/main/java/com/rapidminer/gui/tools/IconSize.java +++ b/src/main/java/com/rapidminer/gui/tools/IconSize.java @@ -27,11 +27,18 @@ */ public enum IconSize { - SMALL(16), LARGE(24), HUGE(48); + /** 16 pixel icons */ + SMALL(16), + + /** 24 pixel icons */ + LARGE(24), + + /** 48 pixel icons */ + HUGE(48); private int size; - private IconSize(int size) { + IconSize(int size) { this.size = size; } diff --git a/src/main/java/com/rapidminer/gui/tools/MultiSwingWorker.java b/src/main/java/com/rapidminer/gui/tools/MultiSwingWorker.java new file mode 100644 index 000000000..b1e2cad0c --- /dev/null +++ b/src/main/java/com/rapidminer/gui/tools/MultiSwingWorker.java @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.gui.tools; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import javax.swing.SwingWorker; + + +/** + * A {@link SwingWorker} with an extra {@link #start()} method that allows to start the execution of the swing worker + * immediately in a cached thread pool with unlimited threads. This is in contrast to the usual {@link #execute()} + * method which allows only {@code 10} swing workers to be running at the same time and puts additional swing workers in + * a waiting queue. + * + * @param + * the result type returned by this {@code SwingWorker's} {@code doInBackground} and {@code get} methods + * @param + * the type used for carrying out intermediate results by this {@code SwingWorker's} {@code publish} and {@code + * process} methods + * @author Gisa Meier + * @since 9.3 + */ +public abstract class MultiSwingWorker extends SwingWorker { + + /** + * this is the {@link ExecutorService} from which all {@link MultiSwingWorker}s are started + */ + private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool(r -> { + Thread thread = new Thread(r, "BackgroundWorker"); + thread.setDaemon(true); + return thread; + }); + + /** + * Starts this {@code SwingWorker} for execution on a background worker thread. There are unlimited + * background worker threads available. This is in contrast to {@link #execute()}, which schedules the + * worker for execution for one of the {@code 10} swing worker threads. + * + *

      + * Note: {@code SwingWorker} is only designed to be executed once. Starting a {@code SwingWorker} more than once + * will not result in invoking the {@code doInBackground} method twice. + */ + public final void start() { + EXECUTOR.execute(this); + } + +} diff --git a/src/main/java/com/rapidminer/gui/tools/ProgressThread.java b/src/main/java/com/rapidminer/gui/tools/ProgressThread.java index 244d9959a..afdc2e718 100644 --- a/src/main/java/com/rapidminer/gui/tools/ProgressThread.java +++ b/src/main/java/com/rapidminer/gui/tools/ProgressThread.java @@ -18,6 +18,7 @@ */ package com.rapidminer.gui.tools; +import java.awt.Window; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -34,11 +35,11 @@ import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.logging.Level; - import javax.swing.SwingUtilities; import javax.swing.event.EventListenerList; import com.rapidminer.RapidMiner; +import com.rapidminer.gui.ApplicationFrame; import com.rapidminer.gui.processeditor.results.ResultDisplay; import com.rapidminer.gui.tools.dialogs.ConfirmDialog; import com.rapidminer.tools.I18N; @@ -131,7 +132,7 @@ public Thread newThread(Runnable r) { private final Set listeners = new CopyOnWriteArraySet<>(); /** true if the task was cancelled */ - protected boolean cancelled = false; + private boolean cancelled = false; /** true if the task is started. (Remains true after canceling.) */ private boolean started = false; @@ -147,7 +148,7 @@ public Thread newThread(Runnable r) { /** * If set to true and {@link #runInForeground} is set to false, the * progress dialog will be shown after the amount of time defined by - * {@link #showDialogTimerDelay} if the progress thread has not finished yet by tehen. The + * {@link #showDialogTimerDelay} if the progress thread has not finished yet by then. The * amount if time can be defined by changing {@link #setShowDialogTimerDelay(long)} which by * default is set to 2000 milliseconds (2 seconds). */ @@ -163,6 +164,11 @@ public Thread newThread(Runnable r) { */ private boolean indeterminate = false;; + /** + * If set to {@code true} and a progress thread is cancelled, there is a popup about cancelling dependent tasks. + */ + private boolean dependencyPopups = true; + /** * Creates a new {@link ProgressThread} instance with the specified {@link I18N} key. Uses its * I18N key as an ID to allow other ProgressThreads to depend on it. @@ -312,16 +318,18 @@ public void setDisplayLabel(String i18nKey) { /** * Note that this method has nothing to do with Thread.start. It merely enqueues this Runnable - * in the Executor's queue. + * in the Executor's queue. It sets cancelled and started to false so that progress threads can be reused. */ public void start() { + cancelled = false; + started = false; // see if task is blocked, if not start it immediately boolean blocked = false; synchronized (LOCK) { blocked = isBlockedByDependencies(); } if (!blocked) { - EXECUTOR.execute(makeWrapper()); + EXECUTOR.execute(makeWrapper(true)); } else { // otherwise add to queue, which is checked once another task finishes execution synchronized (LOCK) { @@ -333,13 +341,16 @@ public void start() { /** * Enqueues this task and waits for its completion. If you call this method, you probably want - * to set the runInForeground flag in the constructor to true. + * to set the runInForeground flag in the constructor to true. This methods sets cancelled and started to false at + * the beginning so that progress threads can be reused. *

      * Be careful when using this method for {@link ProgressThread}s with dependencies, this call * might block for a long time. *

      */ public void startAndWait() { + cancelled = false; + started = false; // set flag indicating we are a busy waiting task - these are not started automatically by // #checkQueueForDependenciesAndExecuteUnblockedTasks() isWaiting = true; @@ -350,7 +361,7 @@ public void startAndWait() { } // no dependency -> start immediately if (!blocked) { - EXECUTOR.submit(makeWrapper()).get(); + EXECUTOR.submit(makeWrapper(true)).get(); return; } synchronized (LOCK) { @@ -367,8 +378,9 @@ public void startAndWait() { // no longer blocked? Execute and wait and afterwards leave loop synchronized (LOCK) { queuedThreads.remove(this); + currentThreads.add(this); } - EXECUTOR.submit(makeWrapper()).get(); + EXECUTOR.submit(makeWrapper(false)).get(); break; } Thread.sleep(BUSY_WAITING_INTERVAL); @@ -402,8 +414,8 @@ public final void cancel() { dependentThreads = checkQueuedThreadDependOnCurrentThread(); } if (dependentThreads) { - if (ConfirmDialog.OK_OPTION != SwingTools.showConfirmDialog("cancel_pg_with_dependencies", - ConfirmDialog.OK_CANCEL_OPTION)) { + if (dependencyPopups && ConfirmDialog.OK_OPTION != SwingTools.showConfirmDialog(getCancellationOwner(), + "cancel_pg_with_dependencies", ConfirmDialog.OK_CANCEL_OPTION)) { return; } else { synchronized (LOCK) { @@ -417,13 +429,29 @@ public final void cancel() { executionCancelled(); currentThreads.remove(this); } else { - // cancel and not started? Can only be in queue - queuedThreads.remove(this); + // cancel and not started? Can be in queue or already in current + boolean found = queuedThreads.remove(this); + if (!found) { + //handle the case that run method not already started + currentThreads.remove(this); + } } } taskCancelled(this); } + /** + * Gets the owner for the dependency cancellation popup. This is the progress thread dialog if it is visible so + * that the cancellation is not shown behind it. + */ + private Window getCancellationOwner() { + ProgressThreadDialog dialog = ProgressThreadDialog.getInstance(); + if (dialog == null || !dialog.isVisible()) { + return ApplicationFrame.getApplicationFrame(); + } + return dialog; + } + /** *

      * ATTENTION: Make sure this is only called from inside a synchronized block! @@ -466,7 +494,6 @@ private static final void removeQueuedThreadsWithDependency(String... ids) { cancelledThreads.add(pg.getID()); } } - // also remove all the ones depending on the ones that have been cancelled. if (!cancelledThreads.isEmpty()) { removeQueuedThreadsWithDependency(cancelledThreads.toArray(new String[cancelledThreads.size()])); @@ -501,9 +528,9 @@ protected void checkCancelled() throws ProgressThreadStoppedException { * Creates a wrapper that executes this class' run method, sets {@link #current} and * subsequently removes it from the list of pending tasks and shows a * {@link ProgressThreadDialog} if necessary. As a side effect, calling this method also results - * in adding this ProgressThread to the list of pending tasks. + * in adding this ProgressThread to the list of pending tasks if addToCurrent is {@code true}. * */ - private Runnable makeWrapper() { + private Runnable makeWrapper(boolean addToCurrent) { // show dialog if wanted if (runInForeground) { SwingUtilities.invokeLater(new Runnable() { @@ -516,9 +543,10 @@ public void run() { }; }); } - - synchronized (LOCK) { - currentThreads.add(ProgressThread.this); + if (addToCurrent) { + synchronized (LOCK) { + currentThreads.add(ProgressThread.this); + } } taskStarted(this); return new Runnable() { @@ -533,24 +561,21 @@ public void run() { } started = true; } - final Timer showProgressTimer = new Timer("show-pg-timer", true); - final TimerTask showProgressTask = new TimerTask() { - - @Override - public void run() { - SwingUtilities.invokeLater(new Runnable() { + Timer showProgressTimer = null; + if (!isRunInForegroundFlagSet() && isStartDialogShowTimer() && !RapidMiner.getExecutionMode().isHeadless()) { + showProgressTimer = new Timer("show-pg-timer", true); + final TimerTask showProgressTask = new TimerTask() { - @Override - public void run() { + @Override + public void run() { + SwingUtilities.invokeLater(() -> { runInForeground = true; if (!ProgressThreadDialog.getInstance().isVisible()) { ProgressThreadDialog.getInstance().setVisible(false, true); } - }; - }); - } - }; - if (!isRunInForegroundFlagSet() && isStartDialogShowTimer() && !RapidMiner.getExecutionMode().isHeadless()) { + }); + } + }; showProgressTimer.schedule(showProgressTask, getShowDialogTimerDelay()); } try { @@ -559,17 +584,24 @@ public void run() { ActionStatisticsCollector.getInstance().log(ActionStatisticsCollector.TYPE_PROGRESS_THREAD, key, "started"); ProgressThread.this.run(); - showProgressTimer.cancel(); + if (showProgressTimer != null) { + showProgressTimer.cancel(); + } ActionStatisticsCollector.getInstance().log(ActionStatisticsCollector.TYPE_PROGRESS_THREAD, key, "completed"); } catch (ProgressThreadStoppedException e) { - showProgressTimer.cancel(); + if (showProgressTimer != null) { + showProgressTimer.cancel(); + } ActionStatisticsCollector.getInstance().log(ActionStatisticsCollector.TYPE_PROGRESS_THREAD, key, "stopped"); - LogService.getRoot().log(Level.FINE, "com.rapidminer.gui.tools.ProgressThread.progress_thread_aborted", + LogService.getRoot().log(Level.FINE, + "com.rapidminer.gui.tools.ProgressThread.progress_thread_aborted", getName()); } catch (Exception e) { - showProgressTimer.cancel(); + if (showProgressTimer != null) { + showProgressTimer.cancel(); + } ActionStatisticsCollector.getInstance().log(ActionStatisticsCollector.TYPE_PROGRESS_THREAD, key, "failed"); LogService.getRoot().log( @@ -733,6 +765,25 @@ public boolean isCancelable() { return isCancelable; } + /** + * Allows to define whether a popup is shown if the user cancels a progress thread that has dependent progress + * threads. By default this is {@code true}. + *
      Note: Changing this is only guaranteed to have an effect before starting the progress thread. + * + * @since 9.3.0 + */ + public void setDependencyPopups(boolean showDependencyPopups) { + this.dependencyPopups = showDependencyPopups; + } + + /** + * @return whether the progress thread opens a popup when it has dependent progress threads and is cancelled or not + * @since 9.3.0 + */ + public boolean showsDependencyPopups() { + return dependencyPopups; + } + /** * @return the currently executed tasks. */ @@ -794,24 +845,18 @@ public static void removeProgressThreadStateListener(ProgressThreadStateListener private static final void checkQueueForDependenciesAndExecuteUnblockedTasks() { // a task has finished, now check tasks in queue if there are ones which are no // longer blocked - List toRemove = new ArrayList<>(); synchronized (LOCK) { - for (ProgressThread pg : queuedThreads) { + for (ProgressThread pg : new ArrayList<>(queuedThreads)) { if (!pg.isBlockedByDependencies()) { // busy waiting tasks should not be started here, they will notice themselves if (!pg.isWaiting()) { - toRemove.add(pg); - EXECUTOR.execute(pg.makeWrapper()); + queuedThreads.remove(pg); + EXECUTOR.execute(pg.makeWrapper(true)); } } } } - // remove here to avoid concurrent modifications - for (ProgressThread pg : toRemove) { - synchronized (LOCK) { - queuedThreads.remove(pg); - } - } + } /** diff --git a/src/main/java/com/rapidminer/gui/tools/ProgressThreadDialog.java b/src/main/java/com/rapidminer/gui/tools/ProgressThreadDialog.java index c65872f21..8389c9f0c 100644 --- a/src/main/java/com/rapidminer/gui/tools/ProgressThreadDialog.java +++ b/src/main/java/com/rapidminer/gui/tools/ProgressThreadDialog.java @@ -93,7 +93,7 @@ public void progressThreadQueued(ProgressThread pg) { @Override public void progressThreadFinished(ProgressThread pg) { - updatePanelInEDT(); + updatePanelAndRemoveInEDT(pg); updateUI(); MAPPING_PG_TO_UI.remove(pg); @@ -101,7 +101,7 @@ public void progressThreadFinished(ProgressThread pg) { @Override public void progressThreadCancelled(ProgressThread pg) { - updatePanelInEDT(); + updatePanelAndRemoveInEDT(pg); updateUI(); MAPPING_PG_TO_UI.remove(pg); @@ -114,6 +114,15 @@ private void updatePanelInEDT() { SwingUtilities.invokeLater(() -> updateThreadPanel(false)); } + private void updatePanelAndRemoveInEDT(ProgressThread pg) { + SwingUtilities.invokeLater(() -> { + updateThreadPanel(false); + // remove pg here again, otherwise it can happen that it is first removed by progressThreadFinished + // and then added again by updateThreadPanel, leading to a memory leak + MAPPING_PG_TO_UI.remove(pg); + }); + } + }); SwingUtilities.invokeLater(ProgressThreadDialog.this::initGUI); @@ -195,7 +204,7 @@ private void updateUI() { if (ProgressThread.isEmpty()) { // close dialog if not opened by user if (!openedByUser) { - SwingUtilities.invokeLater(ProgressThreadDialog.this::dispose); + SwingTools.invokeLater(ProgressThreadDialog.this::dispose); } // hide status bar @@ -206,7 +215,7 @@ private void updateUI() { if (!ProgressThread.isForegroundRunning()) { // close dialog if not opened by user if (!openedByUser) { - SwingUtilities.invokeLater(ProgressThreadDialog.this::dispose); + SwingTools.invokeLater(ProgressThreadDialog.this::dispose); } } diff --git a/src/main/java/com/rapidminer/gui/tools/ProgressThreadDisplay.java b/src/main/java/com/rapidminer/gui/tools/ProgressThreadDisplay.java index d111c360d..9106ba140 100644 --- a/src/main/java/com/rapidminer/gui/tools/ProgressThreadDisplay.java +++ b/src/main/java/com/rapidminer/gui/tools/ProgressThreadDisplay.java @@ -24,8 +24,10 @@ import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; - +import java.util.Map; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; @@ -140,12 +142,19 @@ private String createDisplayLabel() { sb.append("
      "); if (isQueued) { List dependencies = pg.getDependencies(); - if (dependencies.size() > 0) { + + if (!dependencies.isEmpty()) { + ArrayList all = new ArrayList<>(ProgressThread.getQueuedThreads()); + all.addAll(ProgressThread.getCurrentThreads()); + Map keyToName = new HashMap<>(); + for (ProgressThread progressThread : all) { + keyToName.put(progressThread.getID(), progressThread.getName()); + } sb.append(""); sb.append("Depends on: "); for (int i = 0; i < dependencies.size(); i++) { String dependency = dependencies.get(i); - sb.append(I18N.getGUIMessage("gui.progress." + dependency + ".label")); + sb.append(keyToName.getOrDefault(dependency, I18N.getGUIMessage("gui.progress." + dependency + ".label"))); if (i < dependencies.size() - 1) { sb.append(", "); } diff --git a/src/main/java/com/rapidminer/gui/tools/ResourceAction.java b/src/main/java/com/rapidminer/gui/tools/ResourceAction.java index 20c057875..1e6a409af 100644 --- a/src/main/java/com/rapidminer/gui/tools/ResourceAction.java +++ b/src/main/java/com/rapidminer/gui/tools/ResourceAction.java @@ -170,12 +170,7 @@ public ResourceAction(int iconSize, String i18nKey, IconType iconType, Object... String tip = getMessageOrNull(i18nKey + ".tip"); if (tip != null) { if (accStroke != null) { - StringBuilder tipBuilder = new StringBuilder(); - tipBuilder.append(tip); - tipBuilder.append(" ("); - tipBuilder.append(SwingTools.formatKeyStroke(accStroke)); - tipBuilder.append(")"); - tip = tipBuilder.toString(); + tip += " (" + SwingTools.formatKeyStroke(accStroke) + ")"; } putValue(SHORT_DESCRIPTION, i18nArgs == null || i18nArgs.length == 0 ? tip : MessageFormat.format(tip, i18nArgs)); diff --git a/src/main/java/com/rapidminer/gui/tools/SwingTools.java b/src/main/java/com/rapidminer/gui/tools/SwingTools.java index cf08afe6b..fcaee1f3e 100644 --- a/src/main/java/com/rapidminer/gui/tools/SwingTools.java +++ b/src/main/java/com/rapidminer/gui/tools/SwingTools.java @@ -38,17 +38,22 @@ import java.awt.RenderingHints; import java.awt.Shape; import java.awt.Window; +import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; +import java.beans.PropertyChangeListener; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Path; +import java.security.AccessController; +import java.security.PrivilegedAction; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -60,8 +65,9 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; - import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.ImageIcon; @@ -75,6 +81,10 @@ import javax.swing.border.EmptyBorder; import javax.swing.filechooser.FileFilter; import javax.swing.filechooser.FileSystemView; +import javax.swing.text.JTextComponent; + +import org.jdesktop.swingx.plaf.AbstractUIChangeHandler; +import org.jdesktop.swingx.prompt.PromptSupport; import com.rapidminer.RapidMiner; import com.rapidminer.gui.ApplicationFrame; @@ -113,6 +123,7 @@ import com.rapidminer.tools.SystemInfoUtilities.OperatingSystem; import com.rapidminer.tools.Tools; import com.rapidminer.tools.plugin.Plugin; +import com.rapidminer.tools.update.internal.UpdateManagerRegistry; import com.rapidminer.tools.usagestats.ActionStatisticsCollector; @@ -131,7 +142,6 @@ */ public class SwingTools { - /** * Scaling of the GUI of the application. Currently, only non-scaling and Retina displays are supported. Likely to be expanded in the future. * @since 9.0.0 @@ -181,6 +191,9 @@ private static class ResultContainer { public T value; } + /** see {@link #setPrompt(String, JTextComponent)} */ + private static final AtomicBoolean needToMakeSwingXWeakHashMapSynchronized = new AtomicBoolean(false); + private static final Color PROMPT_TEXT_COLOR = new Color(160, 160, 160); /** whether we are on a Mac or not */ private static final boolean IS_MAC = SystemInfoUtilities.getOperatingSystem() == OperatingSystem.OSX; @@ -1567,7 +1580,7 @@ public static void storeLastDirectory(Path selectedFile) { * File selectedFile = fileChooser.getSelectedFile(); * * - * Usually, the method {@link #chooseFile(Component, File, boolean, boolean, FileFilter[])} or + * Usually, the method {@link #chooseFile(Component, String, File, boolean, boolean, FileFilter[], boolean)} or * one of the convenience wrapper methods can be used to do this. This method is only useful if * one is interested, e.g., in the selected file filter. * @@ -1606,7 +1619,7 @@ public static JFileChooser createFileChooser(final String i18nKey, final File fi } } - if (file != null) { + if (file != null && !file.isDirectory()) { fileChooser.setSelectedFile(file); } @@ -2492,7 +2505,7 @@ public static Scaling getGUIScaling() { * official Swing specification and might not be available. * * @return the property to disable clear type on windows or {@code null} - * @see http://stackoverflow.com/questions/18764585/text-antialiasing-broken-in-java-1-7-windows + * @see "http://stackoverflow.com/questions/18764585/text-antialiasing-broken-in-java-1-7-windows" */ private static Object getAaTextProperty() { Object aatextProperty = null; @@ -2528,4 +2541,91 @@ public static Component findDisplayedComponent(Container parent) { return null; } + + /** + * Replaces {@link PromptSupport#setPrompt(String, JTextComponent)}, as that library contains a bug that can cause + * the entire Studio UI to freeze permanently. + * + * @param promptText + * the prompt text or {@code null} if an existing prompt text should be removed + * @param textComponent + * the text component for which the prompt should be, must not be {@code null} + * @since 9.3.0 + */ + public static void setPrompt(String promptText, JTextComponent textComponent) { + if (textComponent == null) { + throw new IllegalArgumentException("textComponent must not be null!"); + } + + PromptSupport.setPrompt(promptText, textComponent); + PromptSupport.setForeground(PROMPT_TEXT_COLOR, textComponent); + PromptSupport.setFontStyle(Font.ITALIC, textComponent); + PromptSupport.setFocusBehavior(PromptSupport.FocusBehavior.SHOW_PROMPT, textComponent); + + // we already exchanged the map with a synchronized map in SwingX, nothing to do anymore + if (!needToMakeSwingXWeakHashMapSynchronized.compareAndSet(false, true)) { + return; + } + + // we need to exchange the WeakHashMap with a synchronized one once (as it's the same instance all the time) + // otherwise we can get a permanent freeze of the UI due to a broken map (https://mailinator.blogspot.com/2009/06/beautiful-race-condition.html) + AccessController.doPrivileged((PrivilegedAction) () -> { + try { + PropertyChangeListener[] uiListener = textComponent.getPropertyChangeListeners("UI"); + for (PropertyChangeListener listener : uiListener) { + if (listener instanceof AbstractUIChangeHandler) { + Field installedWeakHashMap = AbstractUIChangeHandler.class.getDeclaredField("installed"); + installedWeakHashMap.setAccessible(true); + Map mapObject = (Map) installedWeakHashMap.get(listener); + if (mapObject instanceof WeakHashMap) { + installedWeakHashMap.set(listener, Collections.synchronizedMap(mapObject)); + } + } + } + } catch (NoSuchFieldException | IllegalAccessException e) { + // should not happen + LogService.getRoot().log(Level.SEVERE, "com.rapidminer.gui.tools.SwingTools.failed_hashmap_fix", e); + } + + return null; + }); + } + + /** + * Creates an action that, when triggered, searches the marketplace for the given namespace and offers to download + * the extension. + * + * @param i18nKey + * the i18n key that the action uses. Will be resolved to {@code gui.action.{i18nkey}.xyz} where xyz out of + * {label, icon, tip, mne} + * @param namespace + * the namespace, e.g. {@code rmx_my_extension} + * @return the action, never {@code null} + * @since 9.3.0 + */ + public static ResourceAction createMarketplaceDownloadActionForNamespace(String i18nKey, String namespace) { + return new ResourceAction(i18nKey, namespace) { + + @Override + public void loggedActionPerformed(ActionEvent e) { + new ProgressThread("search_extension_on_mp") { + + @Override + public void run() { + try { + String extensionId = UpdateManagerRegistry.INSTANCE.get().getExtensionIdForOperatorPrefix(namespace); + if (extensionId != null) { + UpdateManagerRegistry.INSTANCE.get().showUpdateDialog(false, extensionId); + } else { + SwingTools.showVerySimpleErrorMessage("extension_unknown", namespace); + } + } catch (URISyntaxException | IOException e1) { + SwingTools.showSimpleErrorMessage("marketplace_connection_error", e1); + } + } + }.start(); + } + + }; + } } diff --git a/src/main/java/com/rapidminer/gui/tools/TextFieldWithAction.java b/src/main/java/com/rapidminer/gui/tools/TextFieldWithAction.java index c157515a3..6f9616384 100644 --- a/src/main/java/com/rapidminer/gui/tools/TextFieldWithAction.java +++ b/src/main/java/com/rapidminer/gui/tools/TextFieldWithAction.java @@ -153,7 +153,7 @@ public void paintComponent(Graphics g) { @Override public void mouseReleased(MouseEvent e) { if (!(field.getText().isEmpty() || field.getText() == null) && SwingUtilities.isLeftMouseButton(e)) { - action.actionPerformed(new ActionEvent(actionLabel, ActionEvent.ACTION_PERFORMED, "click")); + action.actionPerformed(new ActionEvent(field, ActionEvent.ACTION_PERFORMED, "click")); } } @@ -273,4 +273,13 @@ public void setForceIcon(ImageIcon forceIcon) { } } + /** + * Returns the actual text field. + * + * @return the field, never {@code null} + * @since 9.2.1 + */ + public JTextField getField() { + return field; + } } diff --git a/src/main/java/com/rapidminer/gui/tools/VersionNumber.java b/src/main/java/com/rapidminer/gui/tools/VersionNumber.java index 380480847..9cb80f2e7 100644 --- a/src/main/java/com/rapidminer/gui/tools/VersionNumber.java +++ b/src/main/java/com/rapidminer/gui/tools/VersionNumber.java @@ -60,7 +60,7 @@ public VersionNumberException(String message) { private static final String RELEASE_CANDIDATE = "rc"; - private static final String SNAPSHOT = "snapshot"; + private static final String SNAPSHOT_TAG = "snapshot"; private int majorNumber; @@ -373,7 +373,7 @@ public final boolean isDevelopmentBuild() { * @return {@code true} if the current version is a snapshot build (exactly if it has a classifier named SNAPSHOT). */ public final boolean isSnapshot() { - return classifier != null && classifier.equalsIgnoreCase(CLASSIFIER_TAG + SNAPSHOT); + return isPreview(SNAPSHOT_TAG); } private boolean isPreview(String tagName) { diff --git a/src/main/java/com/rapidminer/gui/tools/autocomplete/SuccessiveExecutionTimer.java b/src/main/java/com/rapidminer/gui/tools/autocomplete/SuccessiveExecutionTimer.java index 339993e62..5b7785beb 100644 --- a/src/main/java/com/rapidminer/gui/tools/autocomplete/SuccessiveExecutionTimer.java +++ b/src/main/java/com/rapidminer/gui/tools/autocomplete/SuccessiveExecutionTimer.java @@ -21,9 +21,10 @@ import java.awt.event.ActionEvent; import java.lang.ref.WeakReference; import java.util.concurrent.Future; -import javax.swing.SwingWorker; import javax.swing.Timer; +import com.rapidminer.gui.tools.MultiSwingWorker; + /** * Wrapper class for {@link Timer}, that ensures successive execution of given Runnable on the SwingWorker ThreadPool. @@ -114,7 +115,7 @@ private void execute(ActionEvent ae) { if (lastExecution != null && !lastExecution.isDone()) { restart(); } else { - SwingWorker newExecution = new SwingWorker() { + MultiSwingWorker newExecution = new MultiSwingWorker() { @Override protected Void doInBackground() { synchronized (executionLock) { @@ -124,7 +125,7 @@ protected Void doInBackground() { } }; lastExecutionReference = new WeakReference<>(newExecution); - newExecution.execute(); + newExecution.start(); } } } diff --git a/src/main/java/com/rapidminer/gui/tools/bubble/WindowChoreographer.java b/src/main/java/com/rapidminer/gui/tools/bubble/WindowChoreographer.java index 11b15b683..33e930a8d 100644 --- a/src/main/java/com/rapidminer/gui/tools/bubble/WindowChoreographer.java +++ b/src/main/java/com/rapidminer/gui/tools/bubble/WindowChoreographer.java @@ -235,7 +235,7 @@ private void recalculateWindowPosition(Window window, int position) { parentBounds.setLocation(parent.getLocationOnScreen()); int rightX = (int) (parentBounds.getX() + parent.getWidth() - DEFAULT_RIGHT_MARGIN); // this was going crazy sometimes - int topY = Math.max((int) parentBounds.getY(), 0); + int topY = (int) parentBounds.getY(); int yOffset = windowYOffset.get(position); // Recalculate the window positions window.setLocation(rightX - window.getWidth(), topY + yOffset - window.getHeight()); diff --git a/src/main/java/com/rapidminer/gui/tools/color/ColorSlider.java b/src/main/java/com/rapidminer/gui/tools/color/ColorSlider.java index 6b63c3e5e..381497931 100644 --- a/src/main/java/com/rapidminer/gui/tools/color/ColorSlider.java +++ b/src/main/java/com/rapidminer/gui/tools/color/ColorSlider.java @@ -55,7 +55,7 @@ public abstract class ColorSlider extends JComponent { protected int minAmountOfColorPoints; protected int maxAmountOfColorPoints; - private EventListenerList listenerList = new EventListenerList(); + private EventListenerList eventListeners = new EventListenerList(); /** @@ -257,7 +257,7 @@ public void updateUI() { * @param l the ChangeListener to add */ public void addChangeListener(ChangeListener l) { - listenerList.add(ChangeListener.class, l); + eventListeners.add(ChangeListener.class, l); } /** @@ -268,7 +268,7 @@ public void addChangeListener(ChangeListener l) { */ public void removeChangeListener(ChangeListener l) { - listenerList.remove(ChangeListener.class, l); + eventListeners.remove(ChangeListener.class, l); } /** @@ -324,7 +324,7 @@ boolean canAddPoint() { * changes. */ protected void fireStateChanged() { - Object[] listeners = listenerList.getListenerList(); + Object[] listeners = eventListeners.getListenerList(); for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == ChangeListener.class) { ((ChangeListener) listeners[i + 1]).stateChanged(new ChangeEvent(this)); diff --git a/src/main/java/com/rapidminer/gui/tools/components/AbstractLinkButton.java b/src/main/java/com/rapidminer/gui/tools/components/AbstractLinkButton.java index e6c6f1219..8a8a65476 100644 --- a/src/main/java/com/rapidminer/gui/tools/components/AbstractLinkButton.java +++ b/src/main/java/com/rapidminer/gui/tools/components/AbstractLinkButton.java @@ -21,7 +21,6 @@ import java.awt.Color; import java.awt.event.ActionEvent; import java.net.URL; - import javax.swing.Action; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkEvent.EventType; @@ -154,9 +153,23 @@ private static String makeHTML(final Action action) { if (action instanceof ResourceAction) { String iconName = ((ResourceAction) action).getIconName(); if (iconName != null) { - iconUrl = Tools.getResource("icons/16/" + iconName); + String regularIconPath = "icons/16/" + iconName; + String retinaIconPath = "icons/16/@2x/" + iconName; + boolean isRetina = SwingTools.getGUIScaling() == SwingTools.Scaling.RETINA; + String iconLookup = isRetina ? retinaIconPath : regularIconPath; + try { + iconUrl = Tools.getResource(iconLookup); + if (iconUrl == null && isRetina) { + // fallback if @2x icon is missing on retina displays + iconUrl = Tools.getResource(regularIconPath); + } + } catch (NullPointerException e) { + // this can occur if no @2x icon exists on OS X and the security manager throws an NPE + iconUrl = Tools.getResource(regularIconPath); + } } } + if (iconUrl != null) { return String.format(TEMPLATE_ICON_HTML, iconUrl.toString(), name); } else { diff --git a/src/main/java/com/rapidminer/gui/tools/components/FeedbackForm.java b/src/main/java/com/rapidminer/gui/tools/components/FeedbackForm.java index bb57492f3..ab8546407 100644 --- a/src/main/java/com/rapidminer/gui/tools/components/FeedbackForm.java +++ b/src/main/java/com/rapidminer/gui/tools/components/FeedbackForm.java @@ -34,8 +34,6 @@ import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; -import org.jdesktop.swingx.prompt.PromptSupport; - import com.rapidminer.gui.MainFrame; import com.rapidminer.gui.look.Colors; import com.rapidminer.gui.look.RapidLookTools; @@ -189,8 +187,7 @@ private void updateSubmitStatus() { enableSubmit(!freeTextArea.getText().trim().isEmpty() || state != FeedbackState.NONE); } }); - PromptSupport.setFontStyle(Font.ITALIC, freeTextArea); - PromptSupport.setPrompt(I18N.getGUILabel("feedback_form.freetext.prompt"), freeTextArea); + SwingTools.setPrompt(I18N.getGUILabel("feedback_form.freetext.prompt"), freeTextArea); gbc.gridx = 0; gbc.gridy += 1; gbc.weightx = 1.0f; diff --git a/src/main/java/com/rapidminer/gui/tools/components/FixedWidthLabel.java b/src/main/java/com/rapidminer/gui/tools/components/FixedWidthLabel.java index 92e656272..89d4b52c8 100644 --- a/src/main/java/com/rapidminer/gui/tools/components/FixedWidthLabel.java +++ b/src/main/java/com/rapidminer/gui/tools/components/FixedWidthLabel.java @@ -61,4 +61,12 @@ public void setWidth(int width) { public void updateLabel() { super.setText("

      " + rootlessHTML + "
      "); } + + /** + * @return the rootless HTML content w/o the formatting code. Can be {@code null} + * @since 9.3.0 + */ + public String getPlaintext() { + return rootlessHTML; + } } diff --git a/src/main/java/com/rapidminer/gui/tools/components/ToolTipWindow.java b/src/main/java/com/rapidminer/gui/tools/components/ToolTipWindow.java index 691d1351c..a33b0c08d 100644 --- a/src/main/java/com/rapidminer/gui/tools/components/ToolTipWindow.java +++ b/src/main/java/com/rapidminer/gui/tools/components/ToolTipWindow.java @@ -71,6 +71,7 @@ import com.rapidminer.repository.RepositoryLocation; import com.rapidminer.tools.FontTools; import com.rapidminer.tools.RMUrlHandler; +import com.rapidminer.tools.Tools; /** @@ -356,11 +357,11 @@ public void run() { css.addRule("h4 {margin-bottom:0; margin-top:1ex; padding:0}"); css.addRule("p {margin-top:0; margin-bottom:1ex; padding:0}"); css.addRule("ul {margin-top:0; margin-bottom:1ex; list-style-image: url(" - + getClass().getResource("/com/rapidminer/resources/icons/modern/help/circle.png") + ")}"); + + Tools.getResource("icons/help/circle.png") + ")}"); css.addRule("ul li {padding-bottom: 2px}"); css.addRule("li.outPorts {padding-bottom: 0px}"); css.addRule("ul li ul {margin-top:0; margin-bottom:1ex; list-style-image: url(" - + getClass().getResource("/com/rapidminer/resources/icons/modern/help/line.png") + ")"); + + Tools.getResource("icons/help/line.png") + ")"); css.addRule("li ul li {padding-bottom:0}"); tipScrollPane = new JScrollPane(tipPane); diff --git a/src/main/java/com/rapidminer/gui/tools/dnd/ExtendedJListTransferHandler.java b/src/main/java/com/rapidminer/gui/tools/dnd/ExtendedJListTransferHandler.java index 99f8e7064..f21fba44f 100644 --- a/src/main/java/com/rapidminer/gui/tools/dnd/ExtendedJListTransferHandler.java +++ b/src/main/java/com/rapidminer/gui/tools/dnd/ExtendedJListTransferHandler.java @@ -25,11 +25,14 @@ import java.awt.dnd.DragSource; import java.io.IOException; import java.util.Objects; +import java.util.logging.Level; import javax.swing.DefaultListModel; import javax.swing.JComponent; import javax.swing.JList; import javax.swing.TransferHandler; +import com.rapidminer.tools.LogService; + /** * Typed drag & drop handler including a callback option. @@ -151,8 +154,9 @@ public boolean importData(TransferSupport info) { } addCount = values.length; return true; - } catch (UnsupportedFlavorException | IOException ex) { - ex.printStackTrace(); + } catch (UnsupportedFlavorException | IOException e) { + // should never happen, log anyway to be safe + LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.tools.dnd.ExtendedJListTransferHandler.unexpected_error", e); } return false; diff --git a/src/main/java/com/rapidminer/gui/viewer/metadata/MetaDataStatisticsController.java b/src/main/java/com/rapidminer/gui/viewer/metadata/MetaDataStatisticsController.java index 948bae62c..066d40be6 100644 --- a/src/main/java/com/rapidminer/gui/viewer/metadata/MetaDataStatisticsController.java +++ b/src/main/java/com/rapidminer/gui/viewer/metadata/MetaDataStatisticsController.java @@ -19,18 +19,16 @@ package com.rapidminer.gui.viewer.metadata; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; import java.util.List; -import java.util.concurrent.BrokenBarrierException; -import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.FutureTask; import java.util.logging.Level; -import javax.swing.SwingWorker; - import com.rapidminer.example.ExampleSet; import com.rapidminer.example.Statistics; +import com.rapidminer.gui.tools.ProgressThread; import com.rapidminer.gui.tools.UpdateQueue; import com.rapidminer.gui.viewer.metadata.model.AbstractAttributeStatisticsModel; import com.rapidminer.gui.viewer.metadata.model.MetaDataStatisticsModel; @@ -53,7 +51,9 @@ public class MetaDataStatisticsController { * the barrier which is used to update the stats of all {@link AttributeStatisticsPanel}s once * the {@link ExampleSet} statistics have been calculated */ - private CyclicBarrier barrier; + private final CountDownLatch barrier; + + private volatile boolean aborted = false; /** the model backing this */ private MetaDataStatisticsModel model; @@ -61,8 +61,8 @@ public class MetaDataStatisticsController { /** the {@link UpdateQueue} used to sort */ private UpdateQueue sortingQueue; - /** the {@link SwingWorker} to recalculate statistics */ - private SwingWorker worker; + /** the {@link ProgressThread} to recalculate statistics */ + private ProgressThread worker; /** * needed to restore the initial order; faster way as opposed to sorting (which is not easily @@ -73,9 +73,6 @@ public class MetaDataStatisticsController { /** * Creates a new {@link MetaDataStatisticsController} instance. Does not store * - * @param exampleSet - * the {@link ExampleSet} for which the meta data statistics should be created. No - * reference to it is stored to prevent memory leaks. * @param view * @param model */ @@ -86,7 +83,7 @@ public MetaDataStatisticsController(MetaDataStatisticsViewer view, MetaDataStati this.model = model; backupInitialOrderList = new ArrayList<>(); - barrier = createUpdateBarrier(); + barrier = new CountDownLatch(1); // start up sorting queue (for future sorting) sortingQueue = new UpdateQueue("Attribute Sorting"); @@ -99,19 +96,25 @@ public MetaDataStatisticsController(MetaDataStatisticsViewer view, MetaDataStati } /** - * Call to let the controller know that GUI or calculations are done. Once both are done (aka - * this method has been called twice after initialization), the GUI will be notified to display - * everything. + * Call to let the controller know that GUI is done. Once statistics calculation is also done (aka count down latch + * counted down), the GUI will be notified to display everything. + * + * @return whether the statistics calculation has been successful */ - public void waitAtBarrier() { + boolean waitAtBarrier() { try { - // GUI is done, tell barrier so once GUI and calculations are done the GUI can be - // updated + // GUI is done, wait until calculations are done and the GUI can be updated barrier.await(); + if (aborted) { + LogService.getRoot().log(Level.INFO, "com.rapidminer.gui.meta_data_view.calc_cancelled"); + return false; + } else { + updateStatistics(); + return true; + } } catch (InterruptedException e) { LogService.getRoot().log(Level.INFO, "com.rapidminer.gui.meta_data_view.calc_interrupted"); - } catch (BrokenBarrierException e) { - LogService.getRoot().log(Level.INFO, "com.rapidminer.gui.meta_data_view.calc_sync_broken"); + return false; } } @@ -332,7 +335,7 @@ public List getPagedAndVisibleAttributeStatist * Stops the statistics recalculation and the sorting queue. */ void stop() { - worker.cancel(true); + worker.cancel(); sortingQueue.shutdown(); } @@ -444,23 +447,19 @@ private void applyFilters() { * Applies the current sorting. */ private void applySorting() { - sortingQueue.execute(new Runnable() { - - @Override - public void run() { - if (model.getSortingDirection(SortingType.NAME) != SortingDirection.UNDEFINED) { - sortByName(model.getOrderedAttributeStatisticsModels(), model.getSortingDirection(SortingType.NAME)); - } - if (model.getSortingDirection(SortingType.TYPE) != SortingDirection.UNDEFINED) { - sortByType(model.getOrderedAttributeStatisticsModels(), model.getSortingDirection(SortingType.TYPE)); - } - if (model.getSortingDirection(SortingType.MISSING) != SortingDirection.UNDEFINED) { - sortByMissing(model.getOrderedAttributeStatisticsModels(), - model.getSortingDirection(SortingType.MISSING)); - } - - model.fireOrderChangedEvent(); + sortingQueue.execute(() -> { + if (model.getSortingDirection(SortingType.NAME) != SortingDirection.UNDEFINED) { + sortByName(model.getOrderedAttributeStatisticsModels(), model.getSortingDirection(SortingType.NAME)); + } + if (model.getSortingDirection(SortingType.TYPE) != SortingDirection.UNDEFINED) { + sortByType(model.getOrderedAttributeStatisticsModels(), model.getSortingDirection(SortingType.TYPE)); } + if (model.getSortingDirection(SortingType.MISSING) != SortingDirection.UNDEFINED) { + sortByMissing(model.getOrderedAttributeStatisticsModels(), + model.getSortingDirection(SortingType.MISSING)); + } + + model.fireOrderChangedEvent(); }); } @@ -470,21 +469,17 @@ public void run() { * @param direction */ private void sortByName(List list, final SortingDirection direction) { - sort(list, new Comparator() { - - @Override - public int compare(AbstractAttributeStatisticsModel o1, AbstractAttributeStatisticsModel o2) { - int sortResult = o1.getAttribute().getName().compareTo(o2.getAttribute().getName()); - switch (direction) { - case ASCENDING: - return -1 * sortResult; - case DESCENDING: - return sortResult; - case UNDEFINED: - return 0; - default: - return sortResult; - } + sort(list, (o1, o2) -> { + int sortResult = o1.getAttribute().getName().compareTo(o2.getAttribute().getName()); + switch (direction) { + case ASCENDING: + return -1 * sortResult; + case DESCENDING: + return sortResult; + case UNDEFINED: + return 0; + default: + return sortResult; } }); } @@ -495,22 +490,18 @@ public int compare(AbstractAttributeStatisticsModel o1, AbstractAttributeStatist * @param direction */ private void sortByType(List list, final SortingDirection direction) { - sort(list, new Comparator() { - - @Override - public int compare(AbstractAttributeStatisticsModel o1, AbstractAttributeStatisticsModel o2) { - int sortResult = Ontology.ATTRIBUTE_VALUE_TYPE.mapIndex(o1.getAttribute().getValueType()) - .compareTo(Ontology.ATTRIBUTE_VALUE_TYPE.mapIndex(o2.getAttribute().getValueType())); - switch (direction) { - case ASCENDING: - return -1 * sortResult; - case DESCENDING: - return sortResult; - case UNDEFINED: - return 0; - default: - return sortResult; - } + sort(list, (o1, o2) -> { + int sortResult = Ontology.ATTRIBUTE_VALUE_TYPE.mapIndex(o1.getAttribute().getValueType()) + .compareTo(Ontology.ATTRIBUTE_VALUE_TYPE.mapIndex(o2.getAttribute().getValueType())); + switch (direction) { + case ASCENDING: + return -1 * sortResult; + case DESCENDING: + return sortResult; + case UNDEFINED: + return 0; + default: + return sortResult; } }); } @@ -521,32 +512,28 @@ public int compare(AbstractAttributeStatisticsModel o1, AbstractAttributeStatist * @param direction */ private void sortByMissing(List list, final SortingDirection direction) { - sort(list, new Comparator() { - - @Override - public int compare(AbstractAttributeStatisticsModel o1, AbstractAttributeStatisticsModel o2) { - ExampleSet exSet = model.getExampleSetOrNull(); - if (exSet == null) { - return 0; - } + sort(list, (o1, o2) -> { + ExampleSet exSet = model.getExampleSetOrNull(); + if (exSet == null) { + return 0; + } - Double missing1 = exSet.getStatistics(o1.getAttribute(), Statistics.UNKNOWN); - Double missing2 = exSet.getStatistics(o2.getAttribute(), Statistics.UNKNOWN); + Double missing1 = exSet.getStatistics(o1.getAttribute(), Statistics.UNKNOWN); + Double missing2 = exSet.getStatistics(o2.getAttribute(), Statistics.UNKNOWN); - if (missing1 == null || missing2 == null) { + if (missing1 == null || missing2 == null) { + return 0; + } + int sortResult = missing1.compareTo(missing2); + switch (direction) { + case ASCENDING: + return -1 * sortResult; + case DESCENDING: + return sortResult; + case UNDEFINED: return 0; - } - int sortResult = missing1.compareTo(missing2); - switch (direction) { - case ASCENDING: - return -1 * sortResult; - case DESCENDING: - return sortResult; - case UNDEFINED: - return 0; - default: - return sortResult; - } + default: + return sortResult; } }); } @@ -572,54 +559,59 @@ private void restoreInitialStatModelOrder() { } /** - * Creates a {@link CyclicBarrier} which will update the statistics part of all - * {@link AttributeStatisticsPanel}s. - * - * @return + * Update the statistics part of all {@link AttributeStatisticsPanel}s. */ - private CyclicBarrier createUpdateBarrier() { - return new CyclicBarrier(2, new Runnable() { - - @Override - public void run() { - // once both barriers are broken - ExampleSet exampleSet = model.getExampleSetOrNull(); - if (exampleSet == null) { - throw new IllegalArgumentException("model exampleSet must not be null at construction time!"); - } + private void updateStatistics() { + ExampleSet exampleSet = model.getExampleSetOrNull(); + if (exampleSet == null) { + throw new IllegalArgumentException("model exampleSet must not be null at construction time!"); + } - // update stats on all attribute stat models - for (AbstractAttributeStatisticsModel statModel : model.getOrderedAttributeStatisticsModels()) { - statModel.updateStatistics(exampleSet); - } + // update stats on all attribute stat models + for (AbstractAttributeStatisticsModel statModel : model.getOrderedAttributeStatisticsModels()) { + statModel.updateStatistics(exampleSet); + } - // allow sorting and filtering - model.setAllowSortingAndFiltering(); + // allow sorting and filtering + model.setAllowSortingAndFiltering(); - // signal that everything is done - model.fireInitDoneEvent(); - } - }); + // signal that everything is done + model.fireInitDoneEvent(); } /** - * Calculates the statistics of the given {@link ExampleSet} in a {@link SwingWorker}. Once the - * statistics are calculated, will update the stats on all {@link AttributeStatisticsPanel}s. + * Calculates the statistics of the given {@link ExampleSet} in a {@link ProgressThread}. Once the statistics are + * calculated, will update the stats on all {@link AttributeStatisticsPanel}s. * * @param exampleSet + * the example of which to recalculate the statistics */ private void calculateStatistics(final ExampleSet exampleSet) { - worker = new SwingWorker() { + + // wrap into a future task so that cancelling with an interrupt is possible + FutureTask task = new FutureTask<>(() -> { + exampleSet.recalculateAllAttributeStatistics(); + barrier.countDown(); + return null; + }); + + //execute with indeterminate progress thread + worker = new ProgressThread("statistics_calculation") { @Override - protected Void doInBackground() throws Exception { - exampleSet.recalculateAllAttributeStatistics(); - waitAtBarrier(); + public void run() { + task.run(); + } - return null; + @Override + protected void executionCancelled() { + task.cancel(true); + aborted = true; + barrier.countDown(); } }; - worker.execute(); + worker.setIndeterminate(true); + worker.start(); } /** @@ -630,8 +622,8 @@ protected Void doInBackground() throws Exception { * @param comp */ private static void sort(List listOfStatModels, - Comparator comp) { - Collections.sort(listOfStatModels, comp); + Comparator comp) { + listOfStatModels.sort(comp); } } diff --git a/src/main/java/com/rapidminer/gui/viewer/metadata/MetaDataStatisticsViewer.java b/src/main/java/com/rapidminer/gui/viewer/metadata/MetaDataStatisticsViewer.java index afaf0ec2d..18b95aec8 100644 --- a/src/main/java/com/rapidminer/gui/viewer/metadata/MetaDataStatisticsViewer.java +++ b/src/main/java/com/rapidminer/gui/viewer/metadata/MetaDataStatisticsViewer.java @@ -23,7 +23,6 @@ import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; -import java.awt.Font; import java.awt.Graphics; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; @@ -71,23 +70,22 @@ import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; -import org.jdesktop.swingx.prompt.PromptSupport; - import com.rapidminer.example.Attribute; import com.rapidminer.example.AttributeRole; import com.rapidminer.example.ExampleSet; import com.rapidminer.example.set.ExampleSetUtilities; +import com.rapidminer.gui.CleanupRequiringComponent; +import com.rapidminer.gui.actions.CopyStringToClipboardAction; import com.rapidminer.gui.actions.export.PrintableComponent; import com.rapidminer.gui.look.Colors; -import com.rapidminer.gui.CleanupRequiringComponent; import com.rapidminer.gui.tools.ExtendedJScrollPane; +import com.rapidminer.gui.tools.MultiSwingWorker; import com.rapidminer.gui.tools.ResourceAction; import com.rapidminer.gui.tools.ScrollableJPopupMenu; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.gui.tools.TextFieldWithAction; import com.rapidminer.gui.tools.components.DropDownPopupButton; import com.rapidminer.gui.tools.components.DropDownPopupButton.PopupMenuProvider; -import com.rapidminer.gui.viewer.metadata.actions.CopyAllMetaDataToClipboardAction; import com.rapidminer.gui.viewer.metadata.actions.ShowConstructionValueAction; import com.rapidminer.gui.viewer.metadata.event.AttributeStatisticsEvent; import com.rapidminer.gui.viewer.metadata.event.AttributeStatisticsEvent.EventType; @@ -105,6 +103,7 @@ import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; import com.rapidminer.tools.Ontology; +import com.rapidminer.tools.Tools; /** @@ -167,6 +166,9 @@ public class MetaDataStatisticsViewer extends JPanel implements Renderable, Prin /** arrow icon with an arrow pointing down */ static final ImageIcon ICON_ARROW_DOWN = SwingTools.createIcon("16/" + "navigate_down.png"); + /** x-like red icon for cancellation */ + private static final ImageIcon CANCELLATION_ICON = SwingTools.createIcon("16/" + "delete.png"); + /** * icon used in the {@link TextFieldWithAction} when the filter remove action is hovered */ @@ -443,10 +445,11 @@ public void run() { * Creates all {@link AttributeStatisticsPanel}s in a {@link SwingWorker}. */ private void createAttributeStatisticsPanels() { - final SwingWorker worker = new SwingWorker() { + final MultiSwingWorker worker = new MultiSwingWorker() { @Override - protected Void doInBackground() throws Exception { + protected Boolean doInBackground() throws Exception { // prepare attribute lists and settings List orderedAttributeStatisticsModelList = new LinkedList<>(); List listOfAttributes = new LinkedList<>(); @@ -509,8 +512,8 @@ public void modelChanged(final AttributeStatisticsEvent e) { publish(asp); } - controller.waitAtBarrier(); - return null; + // wait until statistics calculation is done or aborted + return controller.waitAtBarrier(); } @Override @@ -526,28 +529,31 @@ public void process(final List list) { @Override protected void done() { try { - // do this to see if any errors popped up while doing the - // above - get(); - - // remove placeholder - attPanel.remove(labelLoading); + boolean statisticsSuccess = get(); + if (statisticsSuccess) { + // remove placeholder + attPanel.remove(labelLoading); - // once all are done refresh the GUI so they are shown - MetaDataStatisticsViewer.this.revalidate(); - MetaDataStatisticsViewer.this.repaint(); + // once all are done refresh the GUI so they are shown + MetaDataStatisticsViewer.this.revalidate(); + MetaDataStatisticsViewer.this.repaint(); - // allow resizing now - resizeNameColumnLabel.setVisible(true); + // allow resizing now + resizeNameColumnLabel.setVisible(true); + } else { + labelLoading.setText(I18N.getMessage(I18N.getGUIBundle(), + "gui.label.meta_data_stats.cancelled.label")); + labelLoading.setIcon(CANCELLATION_ICON); + } } catch (Exception e) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.meta_data_view.calc_error", e); } } - }; - worker.execute(); + worker.start(); } + /** * Setup the GUI. This does NOT include creating the {@link AttributeStatisticsPanel}s, as that * is done via a {@link SwingWorker} above. Reason is that we do not want to risk GUI freezes. @@ -813,9 +819,8 @@ public void actionPerformed(final ActionEvent e) { filterNameField.requestFocusInWindow(); } }); - PromptSupport.setPrompt(I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.filter_field.prompt"), + SwingTools.setPrompt(I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.filter_field.prompt"), filterNameField); - PromptSupport.setFontStyle(Font.ITALIC, filterNameField); ResourceAction deleteFilterAction = new ResourceAction(true, "meta_data_stats.filter_delete") { @@ -868,7 +873,7 @@ public void mousePressed(final MouseEvent e) { private void handlePopup(final MouseEvent e) { if (e.isPopupTrigger()) { JPopupMenu menu = new JPopupMenu(); - menu.add(new CopyAllMetaDataToClipboardAction(model)); + menu.add(new CopyStringToClipboardAction(true, "meta_data_stats.copy_all_metadata", MetaDataStatisticsViewer.this::createClipboardData)); menu.add(new ShowConstructionValueAction(model)); menu.show(e.getComponent(), e.getX(), e.getY()); } @@ -1271,4 +1276,70 @@ public void stop() { controller.stop(); } + /** + * Creates the MD that goes into the clipboard on copy. + */ + private String createClipboardData() { + StringBuilder sb = new StringBuilder(); + for (AbstractAttributeStatisticsModel statModel : model.getOrderedAttributeStatisticsModels()) { + // append general stats like name, type, missing values + sb.append(statModel.getAttribute().getName()); + sb.append("\t"); + + String valueTypeString = Ontology.ATTRIBUTE_VALUE_TYPE.mapIndex(statModel.getAttribute().getValueType()); + valueTypeString = valueTypeString.replaceAll("_", " "); + valueTypeString = String.valueOf(valueTypeString.charAt(0)).toUpperCase() + valueTypeString.substring(1); + sb.append(valueTypeString); + sb.append("\t"); + + sb.append(Tools.formatIntegerIfPossible(statModel.getNumberOfMissingValues())); + sb.append("\t"); + + // if construction is shown, also add it + if (statModel.isShowConstruction()) { + String construction = statModel.getConstruction(); + construction = construction == null ? "-" : construction; + sb.append(construction); + sb.append("\t"); + } + + // append value type specific stuff + if (NumericalAttributeStatisticsModel.class.isAssignableFrom(statModel.getClass())) { + sb.append(((NumericalAttributeStatisticsModel) statModel).getAverage()); + sb.append("\t"); + + sb.append(((NumericalAttributeStatisticsModel) statModel).getDeviation()); + sb.append("\t"); + + sb.append(((NumericalAttributeStatisticsModel) statModel).getMinimum()); + sb.append("\t"); + + sb.append(((NumericalAttributeStatisticsModel) statModel).getMaximum()); + } else if (NominalAttributeStatisticsModel.class.isAssignableFrom(statModel.getClass())) { + int count = 0; + List valueStrings = ((NominalAttributeStatisticsModel) statModel).getValueStrings(); + for (String valueString : valueStrings) { + sb.append(valueString); + if (count < valueStrings.size() - 1) { + sb.append(", "); + } + + count++; + } + } else if (DateTimeAttributeStatisticsModel.class.isAssignableFrom(statModel.getClass())) { + sb.append(((DateTimeAttributeStatisticsModel) statModel).getDuration()); + sb.append("\t"); + + sb.append(((DateTimeAttributeStatisticsModel) statModel).getFrom()); + sb.append("\t"); + + sb.append(((DateTimeAttributeStatisticsModel) statModel).getUntil()); + } + + // next row for next attribute + sb.append(System.lineSeparator()); + } + + return sb.toString(); + } } diff --git a/src/main/java/com/rapidminer/gui/viewer/metadata/actions/CopyAllMetaDataToClipboardAction.java b/src/main/java/com/rapidminer/gui/viewer/metadata/actions/CopyAllMetaDataToClipboardAction.java deleted file mode 100644 index 355830d8d..000000000 --- a/src/main/java/com/rapidminer/gui/viewer/metadata/actions/CopyAllMetaDataToClipboardAction.java +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Copyright (C) 2001-2019 by RapidMiner and the contributors - * - * Complete list of developers available at our web site: - * - * http://rapidminer.com - * - * This program is free software: you can redistribute it and/or modify it under the terms of the - * GNU Affero General Public License as published by the Free Software Foundation, either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License along with this program. - * If not, see http://www.gnu.org/licenses/. -*/ -package com.rapidminer.gui.viewer.metadata.actions; - -import com.rapidminer.gui.tools.ResourceAction; -import com.rapidminer.gui.viewer.metadata.model.AbstractAttributeStatisticsModel; -import com.rapidminer.gui.viewer.metadata.model.DateTimeAttributeStatisticsModel; -import com.rapidminer.gui.viewer.metadata.model.MetaDataStatisticsModel; -import com.rapidminer.gui.viewer.metadata.model.NominalAttributeStatisticsModel; -import com.rapidminer.gui.viewer.metadata.model.NumericalAttributeStatisticsModel; -import com.rapidminer.tools.Ontology; -import com.rapidminer.tools.Tools; - -import java.awt.event.ActionEvent; -import java.util.List; - - -/** - * The action copies all meta data into the clipboard, separating each column via \t so - * pasting into for example Excel is possible. - * - * @author Marco Boeck - * - */ -public class CopyAllMetaDataToClipboardAction extends ResourceAction { - - private static final long serialVersionUID = 6979404131032484600L; - - /** the {@link MetaDataStatisticsModel} model from which the meta data should be copied */ - MetaDataStatisticsModel model; - - /** - * Creates a new {@link CopyAllMetaDataToClipboardAction} instance. - * - * @param model - */ - public CopyAllMetaDataToClipboardAction(MetaDataStatisticsModel model) { - super(true, "meta_data_stats.copy_all_metadata"); - this.model = model; - } - - @Override - public void loggedActionPerformed(ActionEvent e) { - StringBuilder sb = new StringBuilder(); - for (AbstractAttributeStatisticsModel statModel : model.getOrderedAttributeStatisticsModels()) { - // append general stats like name, type, missing values - sb.append(statModel.getAttribute().getName()); - appendTab(sb); - - String valueTypeString = Ontology.ATTRIBUTE_VALUE_TYPE.mapIndex(statModel.getAttribute().getValueType()); - valueTypeString = valueTypeString.replaceAll("_", " "); - valueTypeString = String.valueOf(valueTypeString.charAt(0)).toUpperCase() + valueTypeString.substring(1); - sb.append(valueTypeString); - appendTab(sb); - - sb.append(Tools.formatIntegerIfPossible(statModel.getNumberOfMissingValues())); - appendTab(sb); - - // if construction is shown, also add it - if (statModel.isShowConstruction()) { - String construction = statModel.getConstruction(); - construction = construction == null ? "-" : construction; - sb.append(construction); - appendTab(sb); - } - - // append value type specific stuff - if (NumericalAttributeStatisticsModel.class.isAssignableFrom(statModel.getClass())) { - sb.append(((NumericalAttributeStatisticsModel) statModel).getAverage()); - appendTab(sb); - - sb.append(((NumericalAttributeStatisticsModel) statModel).getDeviation()); - appendTab(sb); - - sb.append(((NumericalAttributeStatisticsModel) statModel).getMinimum()); - appendTab(sb); - - sb.append(((NumericalAttributeStatisticsModel) statModel).getMaximum()); - } else if (NominalAttributeStatisticsModel.class.isAssignableFrom(statModel.getClass())) { - int count = 0; - List valueStrings = ((NominalAttributeStatisticsModel) statModel).getValueStrings(); - for (String valueString : valueStrings) { - sb.append(valueString); - if (count < valueStrings.size() - 1) { - sb.append(", "); - } - - count++; - } - } else if (DateTimeAttributeStatisticsModel.class.isAssignableFrom(statModel.getClass())) { - sb.append(((DateTimeAttributeStatisticsModel) statModel).getDuration()); - appendTab(sb); - - sb.append(((DateTimeAttributeStatisticsModel) statModel).getFrom()); - appendTab(sb); - - sb.append(((DateTimeAttributeStatisticsModel) statModel).getUntil()); - } - - // next row for next attribute - sb.append(System.lineSeparator()); - } - - Tools.copyStringToClipboard(sb.toString()); - } - - /** - * Appends a tabulator symbol to the given {@link StringBuilder}. - * - * @param sb - */ - private static void appendTab(StringBuilder sb) { - sb.append("\t"); - } - -} diff --git a/src/main/java/com/rapidminer/operator/Annotations.java b/src/main/java/com/rapidminer/operator/Annotations.java index 70a41fd55..403efaca1 100644 --- a/src/main/java/com/rapidminer/operator/Annotations.java +++ b/src/main/java/com/rapidminer/operator/Annotations.java @@ -1,26 +1,25 @@ /** * Copyright (C) 2001-2019 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.operator; -import com.rapidminer.example.Attribute; -import com.rapidminer.gui.viewer.MetaDataViewerTableModel; - +import java.io.IOException; +import java.io.InputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; @@ -35,15 +34,32 @@ import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.rapidminer.example.Attribute; +import com.rapidminer.gui.viewer.MetaDataViewerTableModel; + /** * Instances of this class can be used to annotate {@link IOObject}s, {@link Attribute}s, etc. - * + * * @author Simon Fischer, Marius Helf - * */ public class Annotations implements Serializable, Map, Cloneable { + private static final ObjectReader reader; + + private static final ObjectWriter writer; + + static { + ObjectMapper mapper = new ObjectMapper(); + reader = mapper.reader(Annotations.class); + // Remove the forType() call, if we want to support subclasses + writer = mapper.writerWithDefaultPrettyPrinter().withType(Annotations.class); + } + + private static final long serialVersionUID = 1L; public static final String ANNOTATIONS_TAG_NAME = "annotations"; @@ -85,17 +101,17 @@ public class Annotations implements Serializable, Map, Cloneable public static final String KEY_DC_INSTRUCTIONAL_METHOD = "dc.description"; /** Custom keys defined by RapidMiner */ - public static final String[] KEYS_RAPIDMINER_IOOBJECT = { KEY_SOURCE, KEY_COMMENT }; + public static final String[] KEYS_RAPIDMINER_IOOBJECT = {KEY_SOURCE, KEY_COMMENT}; /** Custom keys defined by the Dublin Core standard. */ - public static final String[] KEYS_DUBLIN_CORE = { KEY_DC_AUTHOR, KEY_DC_TITLE, KEY_DC_SUBJECT, KEY_DC_COVERAGE, + public static final String[] KEYS_DUBLIN_CORE = {KEY_DC_AUTHOR, KEY_DC_TITLE, KEY_DC_SUBJECT, KEY_DC_COVERAGE, KEY_DC_DESCRIPTION, KEY_DC_CREATOR, KEY_DC_PUBLISHER, KEY_DC_CONTRIBUTOR, KEY_DC_RIGHTS_HOLDER, KEY_DC_RIGHTS, - KEY_DC_PROVENANCE, KEY_DC_SOURCE, KEY_DC_RELATION, KEY_DC_AUDIENCE, KEY_DC_INSTRUCTIONAL_METHOD, }; + KEY_DC_PROVENANCE, KEY_DC_SOURCE, KEY_DC_RELATION, KEY_DC_AUDIENCE, KEY_DC_INSTRUCTIONAL_METHOD,}; /** All keys that are supposed to be used with {@link IOObject}s. */ - public static final String[] ALL_KEYS_IOOBJECT = { KEY_SOURCE, KEY_COMMENT, KEY_FILENAME, + public static final String[] ALL_KEYS_IOOBJECT = {KEY_SOURCE, KEY_COMMENT, KEY_FILENAME, - KEY_DC_AUTHOR, KEY_DC_TITLE, KEY_DC_SUBJECT, KEY_DC_COVERAGE, KEY_DC_DESCRIPTION, KEY_DC_CREATOR, KEY_DC_PUBLISHER, + KEY_DC_AUTHOR, KEY_DC_TITLE, KEY_DC_SUBJECT, KEY_DC_COVERAGE, KEY_DC_DESCRIPTION, KEY_DC_CREATOR, KEY_DC_PUBLISHER, KEY_DC_CONTRIBUTOR, KEY_DC_RIGHTS_HOLDER, KEY_DC_RIGHTS, KEY_DC_PROVENANCE, KEY_DC_SOURCE, KEY_DC_RELATION, KEY_DC_AUDIENCE, KEY_DC_INSTRUCTIONAL_METHOD, @@ -105,9 +121,9 @@ public class Annotations implements Serializable, Map, Cloneable * Keys that can be assigned to {@link Attribute}s. If you extend this list, also extend * {@link MetaDataViewerTableModel#COLUMN_NAMES}. */ - public static final String[] ALL_KEYS_ATTRIBUTE = { KEY_COMMENT, KEY_UNIT }; + public static final String[] ALL_KEYS_ATTRIBUTE = {KEY_COMMENT, KEY_UNIT}; - private LinkedHashMap keyValueMap = new LinkedHashMap(); + private LinkedHashMap keyValueMap = new LinkedHashMap<>(); /** Pseudo-annotation to be used for attribute names. */ public static final String ANNOTATION_NAME = "Name"; @@ -118,7 +134,7 @@ public Annotations() {} * Clone constructor. */ public Annotations(Annotations annotations) { - this.keyValueMap = new LinkedHashMap(annotations.keyValueMap); + this.keyValueMap = new LinkedHashMap<>(annotations.keyValueMap); } public void setAnnotation(String key, String value) { @@ -130,7 +146,7 @@ public String getAnnotation(String key) { } public List getKeys() { - return new ArrayList(keyValueMap.keySet()); + return new ArrayList<>(keyValueMap.keySet()); } public void removeAnnotation(String key) { @@ -253,14 +269,12 @@ public void parseXML(Element annotationsElem) { } public List getDefinedAnnotationNames() { - List result = new LinkedList(); - result.addAll(keySet()); - return result; + return new LinkedList<>(keySet()); } /* * (non-Javadoc) - * + * * @see java.lang.Object#clone() */ @Override @@ -271,27 +285,45 @@ protected Annotations clone() { /** * Copies all annotations from the input argument to this Annotations object. Existing entries * will be overwritten. - * */ public void addAll(Annotations annotations) { if (annotations != null) { - this.keyValueMap.putAll(annotations); + keyValueMap.putAll(annotations); } } /* * (non-Javadoc) - * + * * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (!(obj instanceof Annotations)) { - return false; - } - return keyValueMap.equals(((Annotations) obj).keyValueMap); + return obj instanceof Annotations && keyValueMap.equals(((Annotations) obj).keyValueMap); + } + + /** + * Retrieve an {@link InputStream} that contains the written object. Use stream for storing in combination with {@link Annotations#fromPropertyStyle(InputStream)}. + * + * @return String with the written {@link Annotations} object + * @throws IOException + * in case writing was not successful + */ + public String asPropertyStyle() throws IOException { + return writer.writeValueAsString(this); } + + /** + * Helper method to load {@link Annotations} that were stored using {@link Annotations#asPropertyStyle()}. + * + * @param in + * {@link InputStream} to read the {@link Annotations} content from + * @return a new {@link Annotations} instance + * @throws IOException + * in case the {@link InputStream} could not be read + */ + public static Annotations fromPropertyStyle(InputStream in) throws IOException { + return reader.readValue(in); + } + } diff --git a/src/main/java/com/rapidminer/operator/DummyOperator.java b/src/main/java/com/rapidminer/operator/DummyOperator.java index b5a8ba153..cc05e6e32 100644 --- a/src/main/java/com/rapidminer/operator/DummyOperator.java +++ b/src/main/java/com/rapidminer/operator/DummyOperator.java @@ -18,7 +18,6 @@ */ package com.rapidminer.operator; -import java.awt.event.ActionEvent; import java.io.IOException; import java.net.URISyntaxException; import java.util.Collections; @@ -26,7 +25,6 @@ import java.util.logging.Level; import com.rapidminer.RapidMiner; -import com.rapidminer.gui.tools.ResourceAction; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.io.process.XMLImporter; import com.rapidminer.operator.ProcessSetupError.Severity; @@ -98,20 +96,7 @@ public void doWork() throws UserError { public List getParameterTypes() { List types = super.getParameterTypes(); ParameterType type = new ParameterTypeLinkButton(PARAMETER_INSTALL_EXTENSION, - I18N.getGUILabel("dummy.parameter.install_extension"), new ResourceAction("install_extension_dummy") { - - private static final long serialVersionUID = 1423879776955743834L; - - @Override - public void loggedActionPerformed(ActionEvent e) { - try { - UpdateManagerRegistry.INSTANCE.get().showUpdateDialog(false, getExtensionId()); - } catch (URISyntaxException | IOException e1) { - SwingTools.showSimpleErrorMessage("dummy.marketplace_connection_error", e1); - } - } - - }); + I18N.getGUILabel("dummy.parameter.install_extension"), SwingTools.createMarketplaceDownloadActionForNamespace("install_extension_dummy", getExtensionId())); type.setExpert(false); types.add(type); return types; diff --git a/src/main/java/com/rapidminer/operator/Operator.java b/src/main/java/com/rapidminer/operator/Operator.java index 7456d5b2f..88e69815b 100644 --- a/src/main/java/com/rapidminer/operator/Operator.java +++ b/src/main/java/com/rapidminer/operator/Operator.java @@ -36,6 +36,7 @@ import java.util.Map; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.transform.stream.StreamResult; @@ -73,10 +74,10 @@ import com.rapidminer.operator.ports.metadata.MetaData; import com.rapidminer.operator.ports.metadata.MetaDataError; import com.rapidminer.operator.ports.metadata.Precondition; -import com.rapidminer.operator.ports.quickfix.ParameterSettingQuickFix; import com.rapidminer.operator.ports.quickfix.QuickFix; import com.rapidminer.operator.ports.quickfix.RelativizeRepositoryLocationQuickfix; import com.rapidminer.operator.preprocessing.filter.AbstractDateDataProcessing; +import com.rapidminer.parameter.CombinedParameterType; import com.rapidminer.parameter.ParameterHandler; import com.rapidminer.parameter.ParameterType; import com.rapidminer.parameter.ParameterTypeAttribute; @@ -672,7 +673,7 @@ public int getApplyCount() { * This parameter is not longer used. */ public Operator cloneOperator(String name, boolean forParallelExecution) { - Operator clone = null; + Operator clone; try { clone = operatorDescription.createOperatorInstance(); } catch (Exception e) { @@ -686,13 +687,7 @@ public Operator cloneOperator(String name, boolean forParallelExecution) { // copy user data entries if (this.userData != null) { - for (String key : this.userData.keySet()) { - UserData data = this.userData.get(key); - if (data != null) { - data = data.copyUserData(clone); - clone.setUserData(key, data); - } - } + this.userData.forEach((k, data) -> {if (data != null) clone.setUserData(k, data.copyUserData(clone));}); } if (forParallelExecution) { @@ -860,74 +855,69 @@ protected void performAdditionalChecks() {} * user. Returns the total number of errors. */ public int checkProperties() { + if (!isEnabled()) { + return 0; + } int errorCount = 0; - if (isEnabled()) { - Iterator i = getParameters().getParameterTypes().iterator(); - while (i.hasNext()) { - ParameterType type = i.next(); - boolean optional = type.isOptional(); - if (!optional) { - boolean parameterSet = getParameters().isSet(type.getKey()); - if (type.getDefaultValue() == null && !parameterSet) { + for (ParameterType type : getParameters().getParameterTypes()) { + boolean optional = type.isOptional(); + boolean hidden = type.isHidden(); + String key = type.getKey(); + if (!optional && !hidden) { + boolean parameterSet = getParameters().isSet(key); + boolean isUndefined = false; + try { + isUndefined = parameterSet + && (type instanceof ParameterTypeAttribute || type instanceof CombinedParameterType) + && "".equals(getParameter(key)); + } catch (UndefinedParameterError e) { + // ignore + } + if (!parameterSet || isUndefined) { + addError(new UndefinedParameterSetupError(this, key)); + errorCount++; + } + } + if (type instanceof ParameterTypeRepositoryLocation) { + String value = getParameters().getParameterOrNull(key); + if (value != null && !((ParameterTypeRepositoryLocation) type).isAllowAbsoluteEntries()) { + if (value.startsWith(RepositoryLocation.REPOSITORY_PREFIX)) { + if (!value.startsWith( + RepositoryLocation.REPOSITORY_PREFIX + RepositoryManager.SAMPLE_REPOSITORY_NAME)) { + addError(new SimpleProcessSetupError(Severity.WARNING, portOwner, + "accessing_repository_by_name", + key.replace('_', ' '), value)); + } + } else if (value.startsWith(String.valueOf(RepositoryLocation.SEPARATOR))) { addError(new SimpleProcessSetupError(Severity.ERROR, portOwner, - Collections.singletonList(new ParameterSettingQuickFix(this, type.getKey())), - "undefined_parameter", new Object[]{type.getKey().replace('_', ' ')})); + Collections.singletonList(new RelativizeRepositoryLocationQuickfix(this, key, value)), + "absolute_repository_location", + key.replace('_', ' '), value)); errorCount++; - } else if (type instanceof ParameterTypeAttribute && parameterSet) { - try { - if ("".equals(getParameter(type.getKey()))) { - addError(new SimpleProcessSetupError(Severity.ERROR, portOwner, - Collections.singletonList(new ParameterSettingQuickFix(this, type.getKey())), - "undefined_parameter", new Object[]{type.getKey().replace('_', ' ')})); - errorCount++; - } - } catch (UndefinedParameterError e) { - // Ignore - } } } - if (type instanceof ParameterTypeRepositoryLocation) { - String value = getParameters().getParameterOrNull(type.getKey()); - if (value != null && !((ParameterTypeRepositoryLocation) type).isAllowAbsoluteEntries()) { - if (value.startsWith(RepositoryLocation.REPOSITORY_PREFIX)) { - if (!value.startsWith( - RepositoryLocation.REPOSITORY_PREFIX + RepositoryManager.SAMPLE_REPOSITORY_NAME)) { - addError(new SimpleProcessSetupError(Severity.WARNING, portOwner, - Collections.emptyList(), "accessing_repository_by_name", - new Object[]{type.getKey().replace('_', ' '), value})); - } - } else if (value.startsWith(String.valueOf(RepositoryLocation.SEPARATOR))) { - addError(new SimpleProcessSetupError(Severity.ERROR, portOwner, - Collections.singletonList( - new RelativizeRepositoryLocationQuickfix(this, type.getKey(), value)), - "absolute_repository_location", - new Object[]{type.getKey().replace('_', ' '), value})); - - } - } - } else if (type instanceof ParameterTypeDateFormat) { - Locale locale = Locale.getDefault(); - try { - int localeIndex; - localeIndex = getParameterAsInt(AbstractDataResultSetReader.PARAMETER_LOCALE); - if (localeIndex >= 0 && localeIndex < AbstractDateDataProcessing.availableLocales.size()) { - locale = AbstractDateDataProcessing.availableLocales.get(localeIndex); - } - } catch (UndefinedParameterError e) { - // ignore and use default locale - } - try { - ParameterTypeDateFormat.createCheckedDateFormat(this, locale, true); - } catch (UserError userError) { - // will not happen because of isSetup + } else if (type instanceof ParameterTypeDateFormat) { + Locale locale = Locale.getDefault(); + try { + int localeIndex; + localeIndex = getParameterAsInt(AbstractDataResultSetReader.PARAMETER_LOCALE); + if (localeIndex >= 0 && localeIndex < AbstractDateDataProcessing.availableLocales.size()) { + locale = AbstractDateDataProcessing.availableLocales.get(localeIndex); } + } catch (UndefinedParameterError e) { + // ignore and use default locale + } + try { + ParameterTypeDateFormat.createCheckedDateFormat(this, locale, true); + } catch (UserError userError) { + // will not happen because of isSetup + } - } else if (!optional && type instanceof ParameterTypeDate) { - String value = getParameters().getParameterOrNull(type.getKey()); - if (value != null && !ParameterTypeDate.isValidDate(value)) { - addError(new SimpleProcessSetupError(Severity.WARNING, portOwner, "invalid_date_format", - new Object[]{type.getKey().replace('_', ' '), value})); - } + } else if (!optional && !hidden && type instanceof ParameterTypeDate) { + String value = getParameters().getParameterOrNull(key); + if (value != null && !ParameterTypeDate.isValidDate(value)) { + addError(new SimpleProcessSetupError(Severity.WARNING, portOwner, "invalid_date_format", + key.replace('_', ' '), value)); } } } @@ -973,11 +963,9 @@ public final void execute() throws OperatorException { return; } - if (getOperatorDescription().getDeprecationInfo() != null) { - if (applyCount.get() == 0) { - getLogger().warning("Deprecation warning for " + getOperatorDescription().getName() + ": " - + getOperatorDescription().getDeprecationInfo()); - } + if (getOperatorDescription().getDeprecationInfo() != null && applyCount.get() == 0) { + getLogger().warning("Deprecation warning for " + getOperatorDescription().getName() + ": " + + getOperatorDescription().getDeprecationInfo()); } getOutputPorts().clear(Port.CLEAR_DATA); @@ -1366,6 +1354,16 @@ public ParameterHandler getParameterHandler() { */ @Override public void setParameters(Parameters parameters) { + if (this.parameters != parameters) { + if (this.parameters != null) { + this.parameters.removeObserver(delegatingParameterObserver); + this.parameters.removeObserver(dirtyObserver); + } + if (parameters != null) { + parameters.addObserver(delegatingParameterObserver, false); + makeDirtyOnUpdate(parameters); + } + } this.parameters = parameters; } @@ -1432,44 +1430,30 @@ public char getParameterAsChar(String key) throws UndefinedParameterError { /** Returns a single named parameter and casts it to int. */ @Override public int getParameterAsInt(String key) throws UndefinedParameterError { - ParameterType type = this.getParameters().getParameterType(key); - String value = getParameter(key); - if (type != null) { - if (type instanceof ParameterTypeCategory) { - String parameterValue = value; - try { - return Integer.valueOf(parameterValue); - } catch (NumberFormatException e) { - ParameterTypeCategory categoryType = (ParameterTypeCategory) type; - return categoryType.getIndex(parameterValue); - } - } - } - try { - return Integer.valueOf(value); - } catch (NumberFormatException e) { - throw new UndefinedParameterError(key, this, "Expected integer but found '" + value + "'."); - } + return getParameterAsIntNumber(key, Integer::valueOf, Integer::intValue); } /** Returns a single named parameter and casts it to long. */ @Override public long getParameterAsLong(String key) throws UndefinedParameterError { + return getParameterAsIntNumber(key, Long::valueOf, Long::valueOf); + } + + private N getParameterAsIntNumber(String key, + Function transformer, + Function caster) throws UndefinedParameterError{ ParameterType type = this.getParameters().getParameterType(key); String value = getParameter(key); - if (type != null) { - if (type instanceof ParameterTypeCategory) { - String parameterValue = value; - try { - return Long.valueOf(parameterValue); - } catch (NumberFormatException e) { - ParameterTypeCategory categoryType = (ParameterTypeCategory) type; - return categoryType.getIndex(parameterValue); - } + if (type instanceof ParameterTypeCategory) { + try { + return transformer.apply(value); + } catch (NumberFormatException e) { + ParameterTypeCategory categoryType = (ParameterTypeCategory) type; + return caster.apply(categoryType.getIndex(value)); } } try { - return Long.valueOf(value); + return transformer.apply(value); } catch (NumberFormatException e) { throw new UndefinedParameterError(key, this, "Expected long but found '" + value + "'."); } @@ -1585,51 +1569,28 @@ public java.io.File getParameterAsFile(String key) throws UserError { * @throws UserError */ @Override - public java.io.File getParameterAsFile(String key, boolean createMissingDirectories) throws UserError { + public File getParameterAsFile(String key, boolean createMissingDirectories) throws UserError { String fileName = getParameter(key); if (fileName == null) { return null; } Process process = getProcess(); + File result; if (process != null) { - File result = process.resolveFileName(fileName); - if (createMissingDirectories) { - File parent = result.getParentFile(); - if (parent != null) { - if (!parent.exists()) { - boolean isDirectoryCreated = parent.mkdirs(); - if (!isDirectoryCreated) { - throw new UserError(null, "io.dir_creation_fail", parent.getAbsolutePath()); - } - } - } - } - return result; + result = process.resolveFileName(fileName); } else { getLogger().fine(getName() + " is not attached to a process. Trying '" + fileName + "' as absolute filename."); - File result = new File(fileName); - if (createMissingDirectories) { - if (result.isDirectory()) { - boolean isDirectoryCreated = result.mkdirs(); - if (!isDirectoryCreated) { - throw new UserError(null, "io.dir_creation_fail", result.getAbsolutePath()); - } - } else { - File parent = result.getParentFile(); - if (parent != null) { - if (!parent.exists()) { - boolean isDirectoryCreated = parent.mkdirs(); - if (!isDirectoryCreated) { - throw new UserError(null, "io.dir_creation_fail", parent.getAbsolutePath()); - } - } + result = new File(fileName); + } - } - } + if (createMissingDirectories) { + File parent = result.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs()) { + throw new UserError(null, "io.dir_creation_fail", parent.getAbsolutePath()); } - return result; } + return result; } /** @@ -2113,13 +2074,7 @@ public ExecutionUnit getConnectionContext() { private final Observer delegatingParameterObserver = new DelegatingObserver<>(this, this); /** Sets the dirty flag on any update. */ @SuppressWarnings("rawtypes") - private final Observer dirtyObserver = new Observer() { - - @Override - public void update(Observable observable, Object arg) { - makeDirty(); - } - }; + private final Observer dirtyObserver = (observable, arg) -> makeDirty(); private ExecutionUnit enclosingExecutionUnit; /** @@ -2280,10 +2235,11 @@ public boolean shouldAutoConnect(InputPort inputPort) { * ProcessRootOperator! */ public Operator getRoot() { - if (getParent() == null) { + OperatorChain parent = getParent(); + if (parent == null) { return this; } else { - return getParent().getRoot(); + return parent.getRoot(); } } @@ -2297,6 +2253,26 @@ public void notifyRenaming(String oldName, String newName) { getParameters().notifyRenaming(oldName, newName); } + /** + * This method is called when the operator given by {@code oldName} (and {@code oldOp} if it is not {@code null}) + * was replaced with the operator described by {@code newName} and {@code newOp}. + * This will inform the {@link Parameters} of the replacing. + * + * @param oldName + * the name of the old operator + * @param oldOp + * the old operator; can be {@code null} + * @param newName + * the name of the new operator + * @param newOp + * the new operator; must not be {@code null} + * @see Parameters#notifyReplacing(String, Operator, String, Operator) + * @since 9.3 + */ + public void notifyReplacing(String oldName, Operator oldOp, String newName, Operator newOp) { + getParameters().notifyReplacing(oldName, oldOp, newName, newOp); + } + @Override protected void fireUpdate(Operator operator) { super.fireUpdate(operator); diff --git a/src/main/java/com/rapidminer/operator/OperatorChain.java b/src/main/java/com/rapidminer/operator/OperatorChain.java index 61f9bccad..b6983e08f 100644 --- a/src/main/java/com/rapidminer/operator/OperatorChain.java +++ b/src/main/java/com/rapidminer/operator/OperatorChain.java @@ -574,12 +574,19 @@ public void assumePreconditionsSatisfied() { @Override public void notifyRenaming(String oldName, String newName) { - for (ExecutionUnit subprocess : subprocesses) { - for (Operator child : subprocess.getOperators()) { - child.notifyRenaming(oldName, newName); - } - } - getParameters().notifyRenaming(oldName, newName); + Arrays.stream(subprocesses).forEach(unit -> unit.getOperators().forEach(op -> op.notifyRenaming(oldName, newName))); + super.notifyRenaming(oldName, newName); + } + + /** + * {@inheritDoc} + * + * Also all inner operators will be notified. + */ + @Override + public void notifyReplacing(String oldName, Operator oldOp, String newName, Operator newOp) { + Arrays.stream(subprocesses).forEach(unit -> unit.getOperators().forEach(op -> op.notifyReplacing(oldName, oldOp, newName, newOp))); + super.notifyReplacing(oldName, oldOp, newName, newOp); } @Override diff --git a/src/main/java/com/rapidminer/operator/SimpleProcessSetupError.java b/src/main/java/com/rapidminer/operator/SimpleProcessSetupError.java index 830f145b1..5116d9b92 100644 --- a/src/main/java/com/rapidminer/operator/SimpleProcessSetupError.java +++ b/src/main/java/com/rapidminer/operator/SimpleProcessSetupError.java @@ -38,7 +38,7 @@ public class SimpleProcessSetupError implements ProcessSetupError { private final Severity severity; public SimpleProcessSetupError(Severity severity, PortOwner owner, String i18nKey, Object... i18nArgs) { - this(severity, owner, Collections. emptyList(), false, i18nKey, i18nArgs); + this(severity, owner, Collections.emptyList(), false, i18nKey, i18nArgs); } public SimpleProcessSetupError(Severity severity, PortOwner owner, List fixes, String i18nKey, diff --git a/src/main/java/com/rapidminer/operator/UndefinedParameterSetupError.java b/src/main/java/com/rapidminer/operator/UndefinedParameterSetupError.java new file mode 100644 index 000000000..60d170f7a --- /dev/null +++ b/src/main/java/com/rapidminer/operator/UndefinedParameterSetupError.java @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.operator; + +import java.util.Collections; + +import com.rapidminer.operator.ports.quickfix.ParameterSettingQuickFix; + +/** + * A setup error that indicates a parameter was not properly set/defined. + * Comes with a {@link ParameterSettingQuickFix} for the specified key. + * + * @author Jan Czogalla + * @since 9.3 + */ +public class UndefinedParameterSetupError extends SimpleProcessSetupError { + + private final String key; + + /** + * Creates an error that comes with a quickfix to set the parameter with the given key + * + * @param operator + * the operator this error refers to + * @param key + * the key of the parameter that is missing + */ + public UndefinedParameterSetupError(Operator operator, String key) { + super(Severity.ERROR, operator.getPortOwner(), + Collections.singletonList(new ParameterSettingQuickFix(operator, key)), + "undefined_parameter", key.replace('_', ' ')); + this.key = key; + } + + /** @return the offending parameter key */ + public String getKey() { + return key; + } +} diff --git a/src/main/java/com/rapidminer/operator/generator/UserSpecificationDataGenerator.java b/src/main/java/com/rapidminer/operator/generator/UserSpecificationDataGenerator.java index 265b34e72..2549003e3 100644 --- a/src/main/java/com/rapidminer/operator/generator/UserSpecificationDataGenerator.java +++ b/src/main/java/com/rapidminer/operator/generator/UserSpecificationDataGenerator.java @@ -228,8 +228,7 @@ public List getParameterTypes() { "This parameter defines the attributes and their values in the single example returned.", new ParameterTypeString(PARAMETER_ATTRIBUTE_NAME, "This is the name of the generated attribute.", false), new ParameterTypeExpression(PARAMETER_ATTRIBUTE_VALUE, - "An expression that is parsed to derive the value of this attribute.", - new OperatorVersionCallable(this)), + "An expression that is parsed to derive the value of this attribute.", new OperatorVersionCallable(this)), false); type.setPrimary(true); types.add(type); diff --git a/src/main/java/com/rapidminer/operator/io/AbstractReader.java b/src/main/java/com/rapidminer/operator/io/AbstractReader.java index f4ba56053..b74103a59 100644 --- a/src/main/java/com/rapidminer/operator/io/AbstractReader.java +++ b/src/main/java/com/rapidminer/operator/io/AbstractReader.java @@ -70,6 +70,8 @@ */ public abstract class AbstractReader extends Operator { + protected static final String TRANSFORMER_THREAD_KEY = "AbstractReader.transform_metadata"; + private final OutputPort outputPort = getOutputPorts().createPort("output"); private final Class generatedClass; @@ -113,17 +115,11 @@ public AbstractReader(OperatorDescription description, Class * @since 9.2.0 */ private ProgressThread createTransformationProgressThread() { - return new ProgressThread("AbstractReader.transform_metadata", false, getName()) { - - @Override - public boolean isIndeterminate() { - return true; - } + ProgressThread progressThread = new ProgressThread(TRANSFORMER_THREAD_KEY, false, getName()) { @Override public void start() { if (transformationScheduled.compareAndSet(false, true)) { - cancelled = false; MetaDataUpdateQueue.registerMDGeneration(getProcess(), this); super.start(); } @@ -140,6 +136,9 @@ public void run() { transformationScheduled.set(false); } }; + progressThread.setDependencyPopups(false); + progressThread.setIndeterminate(true); + return progressThread; } /** @@ -368,6 +367,17 @@ protected void registerOperator(Process process) { cacheDirty = true; } + /** + * Returns the cached {@link MetaData}, if any + * + * @return the cached meta data; might be {@code null} + * @see #isMetaDataCacheable() + * @since 9.3 + */ + protected MetaData getCachedMetaData() { + return cachedMetaData; + } + protected boolean supportsEncoding() { return false; } diff --git a/src/main/java/com/rapidminer/operator/io/ExcelExampleSetWriter.java b/src/main/java/com/rapidminer/operator/io/ExcelExampleSetWriter.java index d6438c16d..ec5582021 100644 --- a/src/main/java/com/rapidminer/operator/io/ExcelExampleSetWriter.java +++ b/src/main/java/com/rapidminer/operator/io/ExcelExampleSetWriter.java @@ -1,33 +1,42 @@ /** * Copyright (C) 2001-2019 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * - * This program is free software: you can redistribute it and/or modify it under the terms of the - * GNU Affero General Public License as published by the Free Software Foundation, either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License along with this program. - * If not, see http://www.gnu.org/licenses/. -*/ + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General + * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses/. + */ package com.rapidminer.operator.io; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; +import java.util.Set; +import org.apache.commons.lang.ArrayUtils; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.CreationHelper; @@ -40,18 +49,33 @@ import com.rapidminer.example.Attribute; import com.rapidminer.example.Example; import com.rapidminer.example.ExampleSet; +import com.rapidminer.operator.MissingIOObjectException; import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorDescription; import com.rapidminer.operator.OperatorException; +import com.rapidminer.operator.OperatorVersion; +import com.rapidminer.operator.ProcessSetupError; import com.rapidminer.operator.ProcessStoppedException; +import com.rapidminer.operator.SimpleProcessSetupError; import com.rapidminer.operator.UserError; +import com.rapidminer.operator.nio.file.FileObject; +import com.rapidminer.operator.nio.file.FileOutputPortHandler; +import com.rapidminer.operator.ports.InputPort; +import com.rapidminer.operator.ports.OutputPort; +import com.rapidminer.operator.ports.PortPairExtender; +import com.rapidminer.operator.ports.metadata.ExampleSetMetaData; +import com.rapidminer.operator.ports.metadata.InputMissingMetaDataError; import com.rapidminer.parameter.ParameterType; import com.rapidminer.parameter.ParameterTypeCategory; import com.rapidminer.parameter.ParameterTypeDateFormat; +import com.rapidminer.parameter.ParameterTypeEnumeration; import com.rapidminer.parameter.ParameterTypeString; +import com.rapidminer.parameter.UndefinedParameterError; +import com.rapidminer.parameter.conditions.BelowOrEqualOperatorVersionCondition; import com.rapidminer.parameter.conditions.EqualTypeCondition; import com.rapidminer.tools.I18N; import com.rapidminer.tools.Ontology; +import com.rapidminer.tools.ValidationUtil; import com.rapidminer.tools.io.Encoding; import jxl.Workbook; @@ -70,184 +94,122 @@ /** *

      - * This operator can be used to write data into Microsoft Excel spreadsheets. This operator creates - * Excel files readable by Excel 95, 97, 2000, XP, 2003 and newer. Missing data values are indicated - * by empty cells. + * This operator can be used to write data into Microsoft Excel spreadsheets. This operator creates Excel files readable + * by Excel 95, 97, 2000, XP, 2003 and newer. Missing data values are indicated by empty cells. *

      * * @author Ingo Mierswa, Nils Woehler */ -public class ExcelExampleSetWriter extends AbstractStreamWriter { +public class ExcelExampleSetWriter extends Operator { + + /** + * Versions > this version do not need the sheet name parameter anymore. It has been replaced by the sheet names + * parameter which can handle multiple sheet names. + */ + private static final OperatorVersion VERSION_WITH_SHEET_NAME_PARAMETER = new OperatorVersion(9, 2, 1); private static final String RAPID_MINER_DATA = "RapidMiner Data"; - /** The parameter name for "The Excel spreadsheet file which should be written." */ + /** + * The parameter name for "The Excel spreadsheet file which should be written." + */ public static final String PARAMETER_EXCEL_FILE = "excel_file"; public static final String FILE_FORMAT_XLS = "xls"; public static final String FILE_FORMAT_XLSX = "xlsx"; - public static final String[] FILE_FORMAT_CATEGORIES = new String[] { FILE_FORMAT_XLS, FILE_FORMAT_XLSX }; + public static final String[] FILE_FORMAT_CATEGORIES = new String[]{FILE_FORMAT_XLS, FILE_FORMAT_XLSX}; public static final int FILE_FORMAT_XLS_INDEX = 0; public static final int FILE_FORMAT_XLSX_INDEX = 1; public static final String PARAMETER_FILE_FORMAT = "file_format"; + public static final String PARAMETER_NUMBER_FORMAT = "number_format"; /** - * @deprecated since 8.2; use {@link ParameterTypeDateFormat#PARAMETER_DATE_FORMAT} instead. + * Only there for compatibility to older versions. Has been replaced by {@link ExcelExampleSetWriter#PARAMETER_SHEET_NAMES}. */ - @Deprecated - public static final String PARAMETER_DATE_FORMAT = ParameterTypeDateFormat.PARAMETER_DATE_FORMAT; - public static final String PARAMETER_NUMBER_FORMAT = "number_format"; public static final String PARAMETER_SHEET_NAME = "sheet_name"; + public static final String PARAMETER_SHEET_NAME_DESCRIPTION = "The name of the first sheet. Note that sheet names " + + "in Excel must be unique and must not exceed 31 characters."; + public static final String PARAMETER_SHEET_NAMES = "sheet_names"; + public static final String PARAMETER_SHEET_NAME_ELEMENT = "sheet_name_element"; + public static final String PARAMETER_OTHER_SHEET_NAMES_DESCRIPTION = "Sheet names can be specified here. " + + "Note that sheet names in Excel must be unique and must not exceed 31 characters."; public static final String DEFAULT_DATE_FORMAT = ParameterTypeDateFormat.DATE_TIME_FORMAT_YYYY_MM_DD_HH_MM_SS; public static final String DEFAULT_NUMBER_FORMAT = "#.0"; + public static final int XLSX_MAX_COLUMNS = 16384; + public static final int XLS_MAX_COLUMNS = 256; /** - * the limit of an excel cell, see the + * the limit of an excel cell, see the * Microsoft limit documentation for more information. */ - private static final int CHARACTER_CELL_LIMIT = 32_767; - private static final String CROP_INDICATOR = "[...]"; + public static final int CHARACTER_CELL_LIMIT = 32_767; + public static final String CROP_INDICATOR = "[...]"; - public ExcelExampleSetWriter(OperatorDescription description) { - super(description); - } + public static final String INPUT_PORT_PREFIX = "input"; + public static final String OUTPUT_PORT_PREFIX = "through"; /** - * Writes the example set into a excel file with XLS format. If you want to write it in XLSX - * format use {@link #writeXLSX(ExampleSet, String, String, String, OutputStream)} - * - * @param exampleSet - * the exampleSet to write. - * @param encoding - * the Charset to use for the file. - * @param out - * the stream to use. - * - * @deprecated please use - * {@link ExcelExampleSetWriter#write(ExampleSet, Charset, OutputStream, Operator)} - * to support checkForStop. + * Excel sheet names must not contain more than 31 characters. */ - @Deprecated - public static void write(ExampleSet exampleSet, Charset encoding, OutputStream out) throws IOException, WriteException { - try { - write(exampleSet, encoding, out, null); - } catch (ProcessStoppedException e) { - // can not happen because we do not deliver an Operator + public static final int MAX_SHEET_NAME_LENGTH = 31; + + protected OutputPort fileOutputPort = getOutputPorts().createPort("file"); + protected FileOutputPortHandler filePortHandler; + + protected final PortPairExtender portExtender = new PortPairExtender(INPUT_PORT_PREFIX, OUTPUT_PORT_PREFIX, getInputPorts(), getOutputPorts(), + new ExampleSetMetaData(), true) { + /** + * Does the same as the original fixNames but replaces the first input/output ports' original names for + * compatibility to older versions. + */ + @Override + protected void fixNames() { + super.fixNames(); + // rename ports for compatibility + getManagedPairs().stream().findFirst().ifPresent(pair -> { + getInputPorts().renamePort(pair.getInputPort(), INPUT_PORT_PREFIX); + getOutputPorts().renamePort(pair.getOutputPort(), OUTPUT_PORT_PREFIX); + } + ); } - } + }; /** - * Writes the example set into a excel file with XLS format. If you want to write it in XLSX - * format use {@link #writeXLSX(ExampleSet, String, String, String, OutputStream)} + * Creates a new ExcelExampleSetWriter with the given description. * - * @param exampleSet - * the exampleSet to write. - * @param encoding - * the Charset to use for the file. - * @param out - * the stream to use. - * @param op - * will be used to provide checkForStop. + * @param description */ - public static void write(ExampleSet exampleSet, Charset encoding, OutputStream out, Operator op) - throws IOException, WriteException, ProcessStoppedException { - try { - // .xls files can only store up to 256 columns, so throw error in case of more - if (exampleSet.getAttributes().allSize() > 256) { - throw new IllegalArgumentException( - I18N.getMessage(I18N.getErrorBundle(), "export.excel.excel_xls_file_exceeds_column_limit")); - } - - WorkbookSettings ws = new WorkbookSettings(); - ws.setEncoding(encoding.name()); - ws.setLocale(Locale.US); - - WritableWorkbook workbook = Workbook.createWorkbook(out, ws); - WritableSheet s = workbook.createSheet(RAPID_MINER_DATA, 0); - writeDataSheet(s, exampleSet, op); - workbook.write(); - workbook.close(); - } finally { - try { - out.close(); - } catch (Exception e) { - // silent. exception will trigger warning anyway - } - } + public ExcelExampleSetWriter(OperatorDescription description) { + super(description); + filePortHandler = new FileOutputPortHandler(this, fileOutputPort, getFileParameterName()); + getTransformer().addGenerationRule(fileOutputPort, FileObject.class); + portExtender.start(); + portExtender.ensureMinimumNumberOfPorts(1); + getTransformer().addRule(portExtender.makePassThroughRule()); } - /** - * Writes the provided {@link ExampleSet} to the {@link WritableSheet}. - * - * @param s - * the DataSheet to be filled - * @param exampleSet - * the data to write - * @param op - * an {@link Operator} of the executing operator to checkForStop - * @throws WriteException - * @throws ProcessStoppedException - */ - private static void writeDataSheet(WritableSheet s, ExampleSet exampleSet, Operator op) - throws WriteException, ProcessStoppedException { - - // Format the Font - WritableFont wf = new WritableFont(WritableFont.ARIAL, 10, WritableFont.BOLD); - WritableCellFormat cf = new WritableCellFormat(wf); - - Iterator a = exampleSet.getAttributes().allAttributes(); - int counter = 0; - while (a.hasNext()) { - Attribute attribute = a.next(); - s.addCell(new Label(counter++, 0, attribute.getName(), cf)); - } - - NumberFormat nf = new NumberFormat(DEFAULT_NUMBER_FORMAT); - WritableCellFormat nfCell = new WritableCellFormat(nf); - WritableFont wf2 = new WritableFont(WritableFont.ARIAL, 10, WritableFont.NO_BOLD); - WritableCellFormat cf2 = new WritableCellFormat(wf2); - - DateFormat df = new DateFormat(ParameterTypeDateFormat.DATE_TIME_FORMAT_YYYY_MM_DD_HH_MM_SS); - - WritableCellFormat dfCell = new WritableCellFormat(df); - int rowCounter = 1; - for (Example example : exampleSet) { - a = exampleSet.getAttributes().allAttributes(); - int columnCounter = 0; - while (a.hasNext()) { - Attribute attribute = a.next(); - if (!Double.isNaN(example.getValue(attribute))) { - if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(attribute.getValueType(), Ontology.NOMINAL)) { - s.addCell(new Label(columnCounter, rowCounter, - stripIfNecessary(replaceForbiddenChars(example.getValueAsString(attribute))), cf2)); - } else if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(attribute.getValueType(), Ontology.DATE_TIME)) { - DateTime dateTime = new DateTime(columnCounter, rowCounter, - new Date((long) example.getValue(attribute)), dfCell); - s.addCell(dateTime); - } else if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(attribute.getValueType(), Ontology.NUMERICAL)) { - Number number = new Number(columnCounter, rowCounter, example.getValue(attribute), nfCell); - s.addCell(number); - } else { - // default: write as a String - s.addCell(new Label(columnCounter, rowCounter, - stripIfNecessary(replaceForbiddenChars(example.getValueAsString(attribute))), cf2)); - } + @Override + public void doWork() throws OperatorException { + OutputStream outputStream = null; + try (OutputStream os = filePortHandler.openSelectedFile()) { + outputStream = os; + writeStream(outputStream); + } catch (IOException e) { + if (outputStream instanceof FileOutputStream) { + throw new UserError(this, e, 322, getParameterAsFile(getFileParameterName()), ""); + } else if (outputStream instanceof ByteArrayOutputStream) { + throw new UserError(this, e, 322, "output stream", ""); + } else { + throw new UserError(this, e, 322, "unknown file or stream", ""); } - columnCounter++; - } - rowCounter++; - - // checkForStop every 100 examples - if (op != null && rowCounter % 100 == 0) { - op.checkForStop(); - } } } - private static String replaceForbiddenChars(String originalValue) { - return originalValue.replace((char) 0, ' '); + @Override + public OperatorVersion[] getIncompatibleVersionChanges() { + return (OperatorVersion[]) ArrayUtils.add(super.getIncompatibleVersionChanges(), + VERSION_WITH_SHEET_NAME_PARAMETER); } @Override @@ -261,19 +223,19 @@ public List getParameterTypes() { "Defines the file format the excel file should be saved as.", FILE_FORMAT_CATEGORIES, FILE_FORMAT_XLSX_INDEX, false)); - List encodingTypes = Encoding.getParameterTypes(this); - for (ParameterType encodingType : encodingTypes) { - encodingType.registerDependencyCondition(new EqualTypeCondition(this, PARAMETER_FILE_FORMAT, FILE_FORMAT_CATEGORIES, - false, new int[] { FILE_FORMAT_XLS_INDEX })); - } - types.addAll(encodingTypes); + types.add(new ParameterTypeEnumeration(PARAMETER_SHEET_NAMES, PARAMETER_OTHER_SHEET_NAMES_DESCRIPTION, + new ParameterTypeString(PARAMETER_SHEET_NAME_ELEMENT, PARAMETER_SHEET_NAME_DESCRIPTION), false)); + + List xlsxTypes = new LinkedList<>(); + + ParameterTypeString sheetNameType = new ParameterTypeString(PARAMETER_SHEET_NAME, + PARAMETER_SHEET_NAME_DESCRIPTION, RAPID_MINER_DATA); + sheetNameType.setExpert(false); + // the sheet name parameter has been replaced by the sheet names parameter in newer versions + sheetNameType.registerDependencyCondition(new BelowOrEqualOperatorVersionCondition(this, + VERSION_WITH_SHEET_NAME_PARAMETER)); + xlsxTypes.add(sheetNameType); - List xlsxTypes = new LinkedList(); - ParameterTypeString sheetName = new ParameterTypeString(PARAMETER_SHEET_NAME, - "The name of the created sheet. Note that sheet name is Excel must not exceed 31 characters.", - RAPID_MINER_DATA); - sheetName.setExpert(false); - xlsxTypes.add(sheetName); ParameterType dateType = new ParameterTypeDateFormat(); dateType.setDefaultValue(DEFAULT_DATE_FORMAT); dateType.setExpert(true); @@ -282,83 +244,245 @@ public List getParameterTypes() { "Specifies the number format for date entries. Default: \"#.0\"", DEFAULT_NUMBER_FORMAT, true)); for (ParameterType xlsxType : xlsxTypes) { xlsxType.registerDependencyCondition(new EqualTypeCondition(this, PARAMETER_FILE_FORMAT, FILE_FORMAT_CATEGORIES, - false, new int[] { FILE_FORMAT_XLSX_INDEX })); + false, FILE_FORMAT_XLSX_INDEX)); } types.addAll(xlsxTypes); + List encodingTypes = Encoding.getParameterTypes(this); + for (ParameterType encodingType : encodingTypes) { + encodingType.registerDependencyCondition(new EqualTypeCondition(this, PARAMETER_FILE_FORMAT, FILE_FORMAT_CATEGORIES, + false, FILE_FORMAT_XLS_INDEX)); + } + types.addAll(encodingTypes); + return types; } @Override + protected void performAdditionalChecks() { + super.performAdditionalChecks(); + // check if any input port is connected + if (portExtender.getManagedPairs().stream().map(PortPairExtender.PortPair::getInputPort). + noneMatch(InputPort::isConnected)) { + // we expect at least one connected input port + portExtender.getManagedPairs().stream().findFirst().ifPresent(portPair -> addError( + new InputMissingMetaDataError(portPair.getInputPort(), ExampleSet.class))); + } + // check if the specified sheet names are valid + try { + validateSheetNames(getSheetNames()); + } catch (UserError userError) { + addError(new SimpleProcessSetupError(ProcessSetupError.Severity.ERROR, getPortOwner(), Collections.emptyList(), true, userError.getMessage())); + } + } + protected String[] getFileExtensions() { - return new String[] { FILE_FORMAT_XLSX, FILE_FORMAT_XLS }; + return new String[]{FILE_FORMAT_XLSX, FILE_FORMAT_XLS}; } - @Override protected String getFileParameterName() { return PARAMETER_EXCEL_FILE; } - @Override - protected void writeStream(ExampleSet exampleSet, OutputStream outputStream) throws OperatorException { + /** + * Creates (but does not add) the file parameter named by {@link #getFileParameterName()} that depends on whether or + * not {@link #fileOutputPort} is connected. + */ + protected ParameterType makeFileParameterType() { + return FileOutputPortHandler.makeFileParameterType(this, getFileParameterName(), () -> fileOutputPort, getFileExtensions()); + } + + protected void writeStream(OutputStream outputStream) throws OperatorException { + portExtender.passDataThrough(); File file = getParameterAsFile(PARAMETER_EXCEL_FILE, true); + List exampleSets = new ArrayList<>(portExtender.getData(ExampleSet.class, true)); + if (exampleSets.isEmpty()) { + throw new MissingIOObjectException(ExampleSet.class); + } + String[] sheetNames = getSheetNames(); + validateSheetNames(sheetNames); if (getParameterAsString(PARAMETER_FILE_FORMAT).equals(FILE_FORMAT_XLSX)) { - // check if date format is valid ParameterTypeDateFormat.createCheckedDateFormat(this, null); - String dateFormat = isParameterSet(ParameterTypeDateFormat.PARAMETER_DATE_FORMAT) ? getParameterAsString(ParameterTypeDateFormat.PARAMETER_DATE_FORMAT) : null; String numberFormat = isParameterSet(PARAMETER_NUMBER_FORMAT) ? getParameterAsString(PARAMETER_NUMBER_FORMAT) : null; - String sheetName = getParameterAsString(PARAMETER_SHEET_NAME); - - if (sheetName.length() > 31) { - throw new UserError(this, "excel_sheet_name_too_long", sheetName, sheetName.length()); + try { + writeXLSX(exampleSets, Arrays.asList(sheetNames), dateFormat, numberFormat, outputStream, this); + } catch (IOException e) { + throw new UserError(this, 303, file.getName(), e.getMessage()); } + } else { + Charset encoding = Encoding.getEncoding(this); try { - writeXLSX(exampleSet, sheetName, dateFormat, numberFormat, outputStream, this); + write(exampleSets, Arrays.asList(sheetNames), encoding, outputStream, this); } catch (WriteException | IOException e) { throw new UserError(this, 303, file.getName(), e.getMessage()); } + } + } + + /** + * Helper method that collects the specified sheet names considering the chosen file format and compatibility + * level. + * + * @return array containing the user specified sheet names for the current settings + * @throws UndefinedParameterError + */ + private String[] getSheetNames() throws UndefinedParameterError { + String[] sheetNames = ParameterTypeEnumeration + .transformString2Enumeration(getParameterAsString(PARAMETER_SHEET_NAMES)); + if (getCompatibilityLevel().isAtMost(VERSION_WITH_SHEET_NAME_PARAMETER)) { + String sheetName = null; + if (getParameterAsString(PARAMETER_FILE_FORMAT).equals(FILE_FORMAT_XLSX)) { + // for older versions and xlsx file type we need to take the sheet name parameter into account + sheetName = getParameterAsString(PARAMETER_SHEET_NAME); + } + if (sheetName == null) { + // for file type xls or if no sheet name has been specified fall back to default sheet name + sheetName = RAPID_MINER_DATA; + } + sheetNames = (String[]) ArrayUtils.add(sheetNames, 0, sheetName); + } + return sheetNames; + } + + /** + * Throws a UserError if any of the given sheet names are too long or if there are duplicate sheet names. + * + * @param sheetNames + * the sheet names to validate + * @throws UserError + */ + private void validateSheetNames(String[] sheetNames) throws UserError { + Set uniqueNames = new HashSet<>(); + for (String name : sheetNames) { + if (name.length() > MAX_SHEET_NAME_LENGTH) { + throw new UserError(this, "excel_sheet_name_too_long", name, name.length()); + } + if (!uniqueNames.add(name)) { + throw new UserError(this, "excel_sheet_name_duplicate", name); + } + } + } + + /** + * Writes the example set into an excel file with XLS format. If you want to write it in XLSX format use {@link + * #writeXLSX(ExampleSet, String, String, String, OutputStream, Operator)} + * + * @param exampleSet + * the exampleSet to write. + * @param encoding + * the Charset to use for the file. + * @param out + * the stream to use. + * @deprecated please use {@link ExcelExampleSetWriter#write(ExampleSet, Charset, OutputStream, Operator)} to + * support checkForStop. + */ + @Deprecated + public static void write(ExampleSet exampleSet, Charset encoding, OutputStream out) throws IOException, WriteException { + try { + write(exampleSet, encoding, out, null); + } catch (ProcessStoppedException e) { + // can not happen because we do not deliver an Operator + } + } + + /** + * Writes the example set into an excel file with XLS format. If you want to write it in XLSX format use {@link + * #writeXLSX(ExampleSet, String, String, String, OutputStream)} + * + * @param exampleSet + * the exampleSet to write. + * @param encoding + * the Charset to use for the file. + * @param out + * the stream to use. + * @param op + * will be used to provide checkForStop. + */ + public static void write(ExampleSet exampleSet, Charset encoding, OutputStream out, Operator op) + throws IOException, WriteException, ProcessStoppedException { + write(Collections.singletonList(exampleSet), Collections.singletonList(RAPID_MINER_DATA), encoding, out, op); + } + + /** + * Writes the list of example sets into an excel file with XLS format. Every example set will occupy a sheet in the + * resulting excel file. Sheets will be named after their corresponding example sets. Optionally sheet names can + * also be specified via the sheetNames string array. If you want to write it in XLSX format use {@link + * #writeXLSX(List, List, String, String, OutputStream, Operator)} + * + * @param exampleSets + * every example set in this list will be written into one sheet of the excel file + * @param sheetNames + * optional sheet names (the order defines which name corresponds to which example set) + * @param encoding + * the Charset to use for the file. + * @param out + * the stream to use. + * @param op + * will be used to provide checkForStop. + * @throws IOException + * @throws WriteException + * @throws ProcessStoppedException + */ + public static void write(List exampleSets, List sheetNames, Charset encoding, OutputStream out, Operator op) + throws IOException, WriteException, ProcessStoppedException { + try { + // .xls files can only store up to XLS_MAX_COLUMNS columns, so throw error in case of more + if (exampleSets.stream().anyMatch(e -> e.getAttributes().allSize() > XLS_MAX_COLUMNS)) { + throw new IllegalArgumentException( + I18N.getMessage(I18N.getErrorBundle(), "export.excel.excel_xls_file_exceeds_column_limit")); + } - } else { WorkbookSettings ws = new WorkbookSettings(); - Charset encoding = Encoding.getEncoding(this); ws.setEncoding(encoding.name()); ws.setLocale(Locale.US); + WritableWorkbook workbook = Workbook.createWorkbook(out, ws); + // write one sheet per example set + int index = 0; + Iterator nameIt = sheetNames.iterator(); + Set usedNames = new HashSet<>(); + for (ExampleSet e : exampleSets) { + String sheetName = createSheetName(nameIt, usedNames, e); + WritableSheet sheet = workbook.createSheet(sheetName, index); + writeDataSheet(sheet, e, op); + index++; + } + workbook.write(); + workbook.close(); + } finally { try { - write(exampleSet, encoding, outputStream, this); - } catch (WriteException | IOException e) { - throw new UserError(this, 303, file.getName(), e.getMessage()); + out.close(); + } catch (Exception e) { + // silent. exception will trigger warning anyway } } } /** - * Writes the example set into a excel file with XLSX format. If you want to write it in XLS - * format use {@link #write(ExampleSet, Charset, OutputStream)}. + * Writes the example set into a excel file with XLSX format. If you want to write it in XLS format use {@link + * #write(ExampleSet, Charset, OutputStream)}. * * @param exampleSet - * the exampleSet to write + * the exampleSet to write * @param sheetName - * name of the excel sheet which will be created. + * name of the excel sheet which will be created. * @param dateFormat - * a string which describes the format used for dates. + * a string which describes the format used for dates. * @param numberFormat - * a string which describes the format used for numbers. + * a string which describes the format used for numbers. * @param outputStream - * the stream to write the file to - * - * @deprecated please use - * {@link ExcelExampleSetWriter#writeXLSX(ExampleSet, String, String, String, OutputStream, Operator)} - * to support checkForStop. + * the stream to write the file to + * @deprecated please use {@link ExcelExampleSetWriter#writeXLSX(ExampleSet, String, String, String, OutputStream, + * Operator)} to support checkForStop. */ @Deprecated public static void writeXLSX(ExampleSet exampleSet, String sheetName, String dateFormat, String numberFormat, - OutputStream outputStream) throws WriteException, IOException { + OutputStream outputStream) throws IOException { try { writeXLSX(exampleSet, sheetName, dateFormat, numberFormat, outputStream, null); } catch (ProcessStoppedException e) { @@ -367,37 +491,94 @@ public static void writeXLSX(ExampleSet exampleSet, String sheetName, String dat } /** - * Writes the example set into a excel file with XLSX format. If you want to write it in XLS - * format use {@link #write(ExampleSet, Charset, OutputStream)}. + * Writes the example set into a excel file with XLSX format. If you want to write it in XLS format use {@link + * #write(ExampleSet, Charset, OutputStream)}. * * @param exampleSet - * the exampleSet to write + * the exampleSet to write * @param sheetName - * name of the excel sheet which will be created. + * name of the excel sheet which will be created. * @param dateFormat - * a string which describes the format used for dates. + * a string which describes the format used for dates. * @param numberFormat - * a string which describes the format used for numbers. + * a string which describes the format used for numbers. * @param outputStream - * the stream to write the file to + * the stream to write the file to * @param op - * needed for checkForStop + * needed for checkForStop */ public static void writeXLSX(ExampleSet exampleSet, String sheetName, String dateFormat, String numberFormat, - OutputStream outputStream, Operator op) throws WriteException, IOException, ProcessStoppedException { - // .xlsx files can only store up to 16384 columns, so throw error in case of more - if (exampleSet.getAttributes().allSize() > 16384) { + OutputStream outputStream, Operator op) throws IOException, ProcessStoppedException { + writeXLSX(Collections.singletonList(exampleSet), Collections.singletonList(sheetName), dateFormat, numberFormat, outputStream, op); + } + + /** + * Writes the list of example sets into an excel file with XLSX format. Every example set will occupy a sheet in the + * resulting excel file. Sheets will be named after their corresponding example sets. Optionally sheet names can + * also be specified via the sheetNames string array. If you want to write it in XLS format use {@link #write(List, + * List, Charset, OutputStream, Operator)} + * + * @param exampleSets + * every example set in this list will be written into one sheet of the excel file. + * @param sheetNames + * optional sheet names (the order defines which name corresponds to which example set) + * @param dateFormat + * a string which describes the format used for dates. + * @param numberFormat + * a string which describes the format used for numbers. + * @param outputStream + * the stream to use. + * @param op + * will be used to provide checkForStop. + * @throws IOException + * @throws ProcessStoppedException + */ + public static void writeXLSX(List exampleSets, List sheetNames, String dateFormat, String numberFormat, + OutputStream outputStream, Operator op) throws IOException, ProcessStoppedException { + // .xlsx files can only store up to XLSX_MAX_COLUMNS columns, so throw error in case of more + if (exampleSets.stream().anyMatch(e -> e.getAttributes().allSize() > XLSX_MAX_COLUMNS)) { throw new IllegalArgumentException( I18N.getMessage(I18N.getErrorBundle(), "export.excel.excel_xlsx_file_exceeds_column_limit")); } try (SXSSFWorkbook workbook = new SXSSFWorkbook(null, SXSSFWorkbook.DEFAULT_WINDOW_SIZE, false, true)) { - Sheet sheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(sheetName)); - dateFormat = dateFormat == null ? DEFAULT_DATE_FORMAT : dateFormat; - numberFormat = numberFormat == null ? "#.0" : numberFormat; - writeXLSXDataSheet(workbook, sheet, dateFormat, numberFormat, exampleSet, op); + + // header font + Font headerFont = workbook.createFont(); + headerFont.setBold(true); + CellStyle headerStyle = workbook.createCellStyle(); + headerStyle.setFont(headerFont); + + // body font + Font bodyFont = workbook.createFont(); + bodyFont.setBold(false); + CreationHelper createHelper = workbook.getCreationHelper(); + + // number format + CellStyle numericalStyle = workbook.createCellStyle(); + numericalStyle.setDataFormat(createHelper.createDataFormat().getFormat(numberFormat)); + numericalStyle.setFont(bodyFont); + + // date format + CellStyle dateStyle = workbook.createCellStyle(); + dateStyle.setDataFormat(createHelper.createDataFormat().getFormat(dateFormat)); + dateStyle.setFont(bodyFont); + + // create nominal cell style + CellStyle nominalStyle = workbook.createCellStyle(); + nominalStyle.setFont(bodyFont); + + // write one sheet per example set + Iterator nameIt = sheetNames.iterator(); + Set usedNames = new HashSet<>(); + for (ExampleSet e : exampleSets) { + String sheetName = createSheetName(nameIt, usedNames, e); + // create a new sheet for the current example set + Sheet sheet = workbook.createSheet(sheetName); + writeXLSXDataSheet(sheet, e, op, headerStyle, dateStyle, numericalStyle, nominalStyle); + } workbook.write(outputStream); } finally { outputStream.flush(); @@ -408,31 +589,27 @@ public static void writeXLSX(ExampleSet exampleSet, String sheetName, String dat /** * Writes the provided {@link ExampleSet} to a XLSX formatted data sheet. * - * @param wb - * the workbook to use * @param sheet - * the excel sheet to write to. - * @param dateFormat - * a string which describes the format used for dates. - * @param numberFormat - * a string which describes the format used for numbers. + * the excel sheet to write to. * @param exampleSet - * the exampleSet to write + * the exampleSet to write * @param op - * needed for checkForStop + * needed for checkForStop + * @param headerStyle + * the style used for headers + * @param dateStyle + * the style used for dates + * @param numericalStyle + * the style used for numericals + * @param nominalStyle + * the style used for nominals * @throws ProcessStoppedException - * if the process was stopped by the user. + * if the process was stopped by the user. * @throws WriteException */ - private static void writeXLSXDataSheet(SXSSFWorkbook wb, Sheet sheet, String dateFormat, String numberFormat, - ExampleSet exampleSet, Operator op) throws WriteException, ProcessStoppedException { - - Font headerFont = wb.createFont(); - headerFont.setBold(true); - - CellStyle headerStyle = wb.createCellStyle(); - headerStyle.setFont(headerFont); - + private static void writeXLSXDataSheet(Sheet sheet, ExampleSet exampleSet, Operator op, + CellStyle headerStyle, CellStyle dateStyle, CellStyle numericalStyle, + CellStyle nominalStyle) throws ProcessStoppedException { // create the header Iterator a = exampleSet.getAttributes().allAttributes(); int columnCounter = 0; @@ -447,26 +624,6 @@ private static void writeXLSXDataSheet(SXSSFWorkbook wb, Sheet sheet, String dat } rowCounter++; - // body font - Font bodyFont = wb.createFont(); - bodyFont.setBold(false); - - CreationHelper createHelper = wb.getCreationHelper(); - - // number format - CellStyle numericalStyle = wb.createCellStyle(); - numericalStyle.setDataFormat(createHelper.createDataFormat().getFormat(numberFormat)); - numericalStyle.setFont(bodyFont); - - // date format - CellStyle dateStyle = wb.createCellStyle(); - dateStyle.setDataFormat(createHelper.createDataFormat().getFormat(dateFormat)); - dateStyle.setFont(bodyFont); - - // create nominal cell style - CellStyle nominalStyle = wb.createCellStyle(); - nominalStyle.setFont(bodyFont); - // fill body for (Example example : exampleSet) { @@ -506,11 +663,11 @@ private static void writeXLSXDataSheet(SXSSFWorkbook wb, Sheet sheet, String dat } /** - * Checks if the given value length is greater than the allowed Excel cell limit ( - * {@value #CHARACTER_CELL_LIMIT}). If it exceeds the limit the string will be stripped. + * Checks if the given value length is greater than the allowed Excel cell limit ( {@value #CHARACTER_CELL_LIMIT}). + * If it exceeds the limit the string will be stripped. * * @param value - * the string value which should be checked + * the string value which should be checked * @return the original string if the character limit is not exceeded, otherwise a stripped one */ private static String stripIfNecessary(String value) { @@ -520,4 +677,121 @@ private static String stripIfNecessary(String value) { return value; } } + + /** + * Helper method that is used to shorten the sheet name in case it has become to long after calling {@link + * ValidationUtil#getNewName(Collection, String)} because of the index that this method might add to the name. + */ + private static String shortenSheetName(String originalName) { + int length = originalName.length(); + if (length > MAX_SHEET_NAME_LENGTH) { + // originalName is of format "name (index)". We want to shorten the name + // but preserve the index + int diff = length - MAX_SHEET_NAME_LENGTH; + int indexStart = originalName.lastIndexOf(" ("); + String shortenedName = originalName.substring(0, indexStart - diff); + String index = originalName.substring(indexStart, length); + return shortenedName + index; + } else { + return originalName; + } + } + + /** + * Creates a sheet name and makes sure that the name is valid and that there are not duplicates. + */ + private static String createSheetName(Iterator nameIt, Set usedNames, ExampleSet e) { + // the default is to use the source's name + String sheetName = e.getSource(); + if (nameIt.hasNext()) { + // if the user has specified a name we will use that instead + sheetName = nameIt.next(); + } + // the following code makes sure sheet name is valid and there are no duplicates + sheetName = WorkbookUtil.createSafeSheetName(sheetName); + sheetName = ValidationUtil.getNewName(usedNames, sheetName, false); + String shortenedName = shortenSheetName(sheetName); + // If shortened name == sheetName we know that the name is still unique. + // Otherwise we need to repeat the procedure to make sure. + while (!shortenedName.equals(sheetName)) { + sheetName = shortenedName; + sheetName = ValidationUtil.getNewName(usedNames, sheetName, false); + shortenedName = shortenSheetName(sheetName); + } + usedNames.add(sheetName); + return sheetName; + } + + /** + * Writes the provided {@link ExampleSet} to the {@link WritableSheet}. + * + * @param s + * the DataSheet to be filled + * @param exampleSet + * the data to write + * @param op + * an {@link Operator} of the executing operator to checkForStop + * @throws WriteException + * @throws ProcessStoppedException + */ + private static void writeDataSheet(WritableSheet s, ExampleSet exampleSet, Operator op) + throws WriteException, ProcessStoppedException { + + // Format the Font + WritableFont wf = new WritableFont(WritableFont.ARIAL, 10, WritableFont.BOLD); + WritableCellFormat cf = new WritableCellFormat(wf); + + Iterator a = exampleSet.getAttributes().allAttributes(); + int counter = 0; + while (a.hasNext()) { + Attribute attribute = a.next(); + s.addCell(new Label(counter++, 0, attribute.getName(), cf)); + } + + NumberFormat nf = new NumberFormat(DEFAULT_NUMBER_FORMAT); + WritableCellFormat nfCell = new WritableCellFormat(nf); + WritableFont wf2 = new WritableFont(WritableFont.ARIAL, 10, WritableFont.NO_BOLD); + WritableCellFormat cf2 = new WritableCellFormat(wf2); + + DateFormat df = new DateFormat(ParameterTypeDateFormat.DATE_TIME_FORMAT_YYYY_MM_DD_HH_MM_SS); + + WritableCellFormat dfCell = new WritableCellFormat(df); + int rowCounter = 1; + for (Example example : exampleSet) { + a = exampleSet.getAttributes().allAttributes(); + int columnCounter = 0; + while (a.hasNext()) { + Attribute attribute = a.next(); + if (!Double.isNaN(example.getValue(attribute))) { + if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(attribute.getValueType(), Ontology.NOMINAL)) { + s.addCell(new Label(columnCounter, rowCounter, + stripIfNecessary(replaceForbiddenChars(example.getValueAsString(attribute))), cf2)); + } else if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(attribute.getValueType(), Ontology.DATE_TIME)) { + DateTime dateTime = new DateTime(columnCounter, rowCounter, + new Date((long) example.getValue(attribute)), dfCell); + s.addCell(dateTime); + } else if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(attribute.getValueType(), Ontology.NUMERICAL)) { + Number number = new Number(columnCounter, rowCounter, example.getValue(attribute), nfCell); + s.addCell(number); + } else { + // default: write as a String + s.addCell(new Label(columnCounter, rowCounter, + stripIfNecessary(replaceForbiddenChars(example.getValueAsString(attribute))), cf2)); + } + } + columnCounter++; + } + rowCounter++; + + // checkForStop every 100 examples + if (op != null && rowCounter % 100 == 0) { + op.checkForStop(); + } + } + } + + private static String replaceForbiddenChars(String originalValue) { + return originalValue.replace((char) 0, ' '); + } + } diff --git a/src/main/java/com/rapidminer/operator/io/RepositorySource.java b/src/main/java/com/rapidminer/operator/io/RepositorySource.java index 7b04875b9..ba4f30315 100644 --- a/src/main/java/com/rapidminer/operator/io/RepositorySource.java +++ b/src/main/java/com/rapidminer/operator/io/RepositorySource.java @@ -24,6 +24,8 @@ import java.util.Map; import java.util.logging.Level; +import com.rapidminer.connection.ConnectionInformationContainerIOObject; +import com.rapidminer.gui.tools.ProgressThread; import com.rapidminer.operator.Annotations; import com.rapidminer.operator.IOObject; import com.rapidminer.operator.InvalidRepositoryEntryError; @@ -45,6 +47,7 @@ import com.rapidminer.repository.RepositoryEntryWrongTypeException; import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; +import com.rapidminer.tools.usagestats.ActionStatisticsCollector; /** @@ -63,6 +66,8 @@ public class RepositorySource extends AbstractReader { public RepositorySource(OperatorDescription description) { super(description, IOObject.class); + // reuse precheck thread + getTransformer().addRule(getPrecheckThread()::start); } @Override @@ -85,6 +90,7 @@ public MetaData getGeneratedMetaData() throws OperatorException { } } } + metaData.getAnnotations().setAnnotation(Annotations.KEY_SOURCE, entry.getLocation().toString()); return metaData; } catch (RepositoryException e) { getLogger().log(Level.INFO, "Error retrieving meta data from " + entry.getLocation() + ": " + e, e); @@ -98,15 +104,65 @@ protected boolean isMetaDataCacheable() { return true; } + /** + * Returns a {@link ProgressThread} that checks if the repository entry selected for the parameter + * {@value #PARAMETER_REPOSITORY_ENTRY} exists and compares that state to the previous one. Will mark the cache as + * dirty if the state changed. + * + * @return the progress thread + * @see #getCachedMetaData() + * @since 9.3 + */ + private ProgressThread getPrecheckThread() { + ProgressThread precheckThread = new ProgressThread("RepositorySource.precheck_metadata", false, getName()) { + + @Override + public void run() { + String repoLocationParam = null; + boolean wasBrokenBefore = false; + String unreplacedParameter = null; + try { + repoLocationParam = getParameter(PARAMETER_REPOSITORY_ENTRY); + // cannot use the repoLocationParam to make the cache dirty via setParameter below: Macros are + // already resolved there -> create unreplaced parameter + unreplacedParameter = getParameters().getParameter(PARAMETER_REPOSITORY_ENTRY); + wasBrokenBefore = getCachedMetaData() == null || IOObject.class.equals(getCachedMetaData().getObjectClass()); + checkCancelled(); + IOObjectEntry entry = getRepositoryEntry(); + // retrieve the meta data to prevent an infinite update loop in case the entry is there but the meta data is not + entry.retrieveMetaData(); + checkCancelled(); + if (wasBrokenBefore) { + // make cache dirty by setting the parameter to its current state; no change in value happens + setParameter(PARAMETER_REPOSITORY_ENTRY, unreplacedParameter); + transformMetaData(); + } + } catch (RepositoryException e) { + checkCancelled(); + if (!wasBrokenBefore && repoLocationParam != null) { + // make cache dirty by setting the parameter to its current state; no change in value happens + setParameter(PARAMETER_REPOSITORY_ENTRY, unreplacedParameter); + transformMetaData(); + } + } catch (UserError e) { + // ignore; this will be handled elsewhere + } + } + }; + precheckThread.addDependency(TRANSFORMER_THREAD_KEY); + precheckThread.setIndeterminate(true); + return precheckThread; + } + private IOObjectEntry getRepositoryEntry() throws RepositoryException, UserError { RepositoryLocation location = getParameterAsRepositoryLocation(PARAMETER_REPOSITORY_ENTRY); Entry entry = location.locateEntry(); if (entry == null) { - throw new RepositoryEntryNotFoundException("Entry '" + location + "' does not exist."); + throw new RepositoryEntryNotFoundException(location); } else if (entry instanceof IOObjectEntry) { return (IOObjectEntry) entry; } else { - throw new RepositoryEntryWrongTypeException("Entry '" + location + "' is not a data entry, but " + entry.getType()); + throw new RepositoryEntryWrongTypeException(location, IOObjectEntry.TYPE_NAME, entry.getType()); } } @@ -139,12 +195,23 @@ public IOObject read() throws OperatorException { try { final IOObject data = getRepositoryEntry().retrieveData(null); data.getAnnotations().setAnnotation(Annotations.KEY_SOURCE, getRepositoryEntry().getLocation().toString()); + logConnection(data); return data; } catch (RepositoryException e) { throw new UserError(this, e, 312, getParameterAsString(PARAMETER_REPOSITORY_ENTRY), e.getMessage()); } } + /** + * Logs if the data is a connection. + */ + private void logConnection(IOObject data) { + if (data instanceof ConnectionInformationContainerIOObject) { + ActionStatisticsCollector.INSTANCE.logNewConnection(this, + ((ConnectionInformationContainerIOObject) data).getConnectionInformation()); + } + } + @Override public List getParameterTypes() { List types = super.getParameterTypes(); diff --git a/src/main/java/com/rapidminer/operator/io/RepositoryStorer.java b/src/main/java/com/rapidminer/operator/io/RepositoryStorer.java index ca1c19285..e502032da 100644 --- a/src/main/java/com/rapidminer/operator/io/RepositoryStorer.java +++ b/src/main/java/com/rapidminer/operator/io/RepositoryStorer.java @@ -20,6 +20,7 @@ import java.util.List; +import com.rapidminer.connection.ConnectionInformationContainerIOObject; import com.rapidminer.operator.IOObject; import com.rapidminer.operator.OperatorDescription; import com.rapidminer.operator.OperatorException; @@ -29,6 +30,7 @@ import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; import com.rapidminer.repository.RepositoryManager; +import com.rapidminer.tools.usagestats.ActionStatisticsCollector; /** @@ -49,12 +51,23 @@ public RepositoryStorer(OperatorDescription description) { public IOObject write(IOObject ioobject) throws OperatorException { try { RepositoryLocation location = getParameterAsRepositoryLocation(PARAMETER_REPOSITORY_ENTRY); + logConnection(ioobject); return RepositoryManager.getInstance(null).store(ioobject, location, this); } catch (RepositoryException e) { throw new UserError(this, e, 315, getParameterAsString(PARAMETER_REPOSITORY_ENTRY), e.getMessage()); } } + /** + * Logs if the object is a connection. + */ + private void logConnection(IOObject ioObject) { + if (ioObject instanceof ConnectionInformationContainerIOObject) { + ActionStatisticsCollector.INSTANCE.logNewConnection(this, + ((ConnectionInformationContainerIOObject) ioObject).getConnectionInformation()); + } + } + @Override public List getParameterTypes() { List types = super.getParameterTypes(); diff --git a/src/main/java/com/rapidminer/operator/meta/ParameterIteratingOperatorChain.java b/src/main/java/com/rapidminer/operator/meta/ParameterIteratingOperatorChain.java index 281423c33..b3d1d2ac5 100644 --- a/src/main/java/com/rapidminer/operator/meta/ParameterIteratingOperatorChain.java +++ b/src/main/java/com/rapidminer/operator/meta/ParameterIteratingOperatorChain.java @@ -44,10 +44,9 @@ import com.rapidminer.parameter.ParameterType; import com.rapidminer.parameter.ParameterTypeCategory; import com.rapidminer.parameter.ParameterTypeConfiguration; -import com.rapidminer.parameter.ParameterTypeInnerOperator; import com.rapidminer.parameter.ParameterTypeList; +import com.rapidminer.parameter.ParameterTypeOperatorParameterTupel; import com.rapidminer.parameter.ParameterTypeParameterValue; -import com.rapidminer.parameter.ParameterTypeString; import com.rapidminer.parameter.ParameterTypeTupel; import com.rapidminer.parameter.UndefinedParameterError; import com.rapidminer.parameter.conditions.AboveOperatorVersionCondition; @@ -91,10 +90,6 @@ public abstract class ParameterIteratingOperatorChain extends OperatorChain impl private static final String PARAMETER_OPERATOR_PARAMETER_PAIR = "operator_parameter_pair"; - private static final String PARAMETER_OPERATOR = "operator_name"; - - private static final String PARAMETER_PARAMETER = "parameter_name"; - public static final String PARAMETER_ERROR_HANDLING = "error_handling"; public static final String[] ERROR_HANDLING_METHOD = new String[] { "fail on error", "ignore error" }; @@ -308,9 +303,7 @@ public List getParameterTypes() { type.setExpert(false); types.add(type); type = new ParameterTypeList(ParameterConfigurator.PARAMETER_PARAMETERS, "The parameters.", - new ParameterTypeTupel(PARAMETER_OPERATOR_PARAMETER_PAIR, "The operator and it's parameter", - new ParameterTypeInnerOperator(PARAMETER_OPERATOR, "The operator."), - new ParameterTypeString(PARAMETER_PARAMETER, "The parameter.")), + new ParameterTypeOperatorParameterTupel(PARAMETER_OPERATOR_PARAMETER_PAIR, "The operator and it's parameter"), new ParameterTypeParameterValue(ParameterConfigurator.PARAMETER_VALUES, "The value specifications for the parameters.")); type.setHidden(true); diff --git a/src/main/java/com/rapidminer/operator/nio/StoreDataWizardStep.java b/src/main/java/com/rapidminer/operator/nio/StoreDataWizardStep.java index 5e66103b6..cda2a0b25 100644 --- a/src/main/java/com/rapidminer/operator/nio/StoreDataWizardStep.java +++ b/src/main/java/com/rapidminer/operator/nio/StoreDataWizardStep.java @@ -115,7 +115,7 @@ public void run() { // Switch to result try { Entry entry = location.locateEntry(); - if (entry != null && entry instanceof IOObjectEntry) { + if (entry instanceof IOObjectEntry) { OpenAction.showAsResult((IOObjectEntry) entry); } } catch (RepositoryException e) { diff --git a/src/main/java/com/rapidminer/operator/nio/file/FileInputPortHandler.java b/src/main/java/com/rapidminer/operator/nio/file/FileInputPortHandler.java index d59ad5e3c..f8c52648e 100644 --- a/src/main/java/com/rapidminer/operator/nio/file/FileInputPortHandler.java +++ b/src/main/java/com/rapidminer/operator/nio/file/FileInputPortHandler.java @@ -141,7 +141,7 @@ public boolean isFileSpecified() { return operator.isParameterSet(fileParameterName); } else { try { - return fileInputPort.getData(IOObject.class) != null; + return fileInputPort.getDataOrNull(FileObject.class) != null; } catch (OperatorException e) { return false; } diff --git a/src/main/java/com/rapidminer/operator/performance/AbstractPerformanceEvaluator.java b/src/main/java/com/rapidminer/operator/performance/AbstractPerformanceEvaluator.java index c4d67fb01..bbe43f534 100644 --- a/src/main/java/com/rapidminer/operator/performance/AbstractPerformanceEvaluator.java +++ b/src/main/java/com/rapidminer/operator/performance/AbstractPerformanceEvaluator.java @@ -107,7 +107,9 @@ public abstract class AbstractPerformanceEvaluator extends Operator implements C /** Indicates if example weights should be used for performance calculations. */ private static final String PARAMETER_USE_EXAMPLE_WEIGHTS = "use_example_weights"; - private InputPort exampleSetInput = getInputPorts().createPort("labelled data"); + public static final String INPUT_PORT_LABELLED_DATA = "labelled data"; + + private InputPort exampleSetInput = getInputPorts().createPort(INPUT_PORT_LABELLED_DATA); private InputPort performanceInput = getInputPorts().createPort("performance"); private OutputPort performanceOutput = getOutputPorts().createPort("performance"); private OutputPort exampleSetOutput = getOutputPorts().createPort("example set"); @@ -560,7 +562,7 @@ public int checkProperties() { .getName(), "true")); } } - return super.checkDeprecations(); + return super.checkDeprecations() + super.checkProperties(); } @Override diff --git a/src/main/java/com/rapidminer/operator/performance/AreaUnderCurve.java b/src/main/java/com/rapidminer/operator/performance/AreaUnderCurve.java index 149ff59f7..70f9733b7 100644 --- a/src/main/java/com/rapidminer/operator/performance/AreaUnderCurve.java +++ b/src/main/java/com/rapidminer/operator/performance/AreaUnderCurve.java @@ -18,6 +18,10 @@ */ package com.rapidminer.operator.performance; +import java.io.ObjectStreamException; +import java.util.LinkedList; +import java.util.List; + import com.rapidminer.example.Example; import com.rapidminer.example.ExampleSet; import com.rapidminer.operator.OperatorException; @@ -26,10 +30,6 @@ import com.rapidminer.tools.math.ROCData; import com.rapidminer.tools.math.ROCDataGenerator; -import java.io.ObjectStreamException; -import java.util.LinkedList; -import java.util.List; - /** * This criterion calculates the area under the ROC curve. @@ -101,6 +101,11 @@ public AreaUnderCurve() { method = ROCBias.OPTIMISTIC; } + /** + * True iff the user specified the positive class name. + */ + private boolean userSpecifiedPositiveClass; + public AreaUnderCurve(ROCBias method) { this.method = method; } @@ -117,10 +122,13 @@ public AreaUnderCurve(AreaUnderCurve aucObject) { @Override public void startCounting(ExampleSet exampleSet, boolean useExampleWeights) throws OperatorException { super.startCounting(exampleSet, useExampleWeights); + this.positiveClass = userSpecifiedPositiveClass ? positiveClass : + exampleSet.getAttributes().getPredictedLabel().getMapping().getPositiveString(); // create ROC data - this.rocData.add(rocDataGenerator.createROCData(exampleSet, useExampleWeights, method)); + // using null will make the rocDataGenerator fall back to the label's intern mapping + this.rocData.add(rocDataGenerator.createROCData(exampleSet, useExampleWeights, method, + userSpecifiedPositiveClass ? positiveClass : null)); this.auc = rocDataGenerator.calculateAUC(this.rocData.getLast()); - this.positiveClass = exampleSet.getAttributes().getPredictedLabel().getMapping().getPositiveString(); } /** Does nothing. Everything is done in {@link #startCounting(ExampleSet, boolean)}. */ @@ -186,4 +194,15 @@ public ROCDataGenerator getRocDataGenerator() { public void readResolve() throws ObjectStreamException { rocDataGenerator = new ROCDataGenerator(1.0d, 1.0d); } + + /** + * Sets a user defined positive class (overrides the labels original mapping). + * + * @param positiveClass + * User defined positive class name. If {@code null}, the last user specified name is deleted. + */ + public void setUserDefinedPositiveClassName(String positiveClass) { + this.positiveClass = positiveClass; + userSpecifiedPositiveClass = positiveClass != null; + } } diff --git a/src/main/java/com/rapidminer/operator/performance/BinaryClassificationPerformance.java b/src/main/java/com/rapidminer/operator/performance/BinaryClassificationPerformance.java index 352b1d618..899ef2184 100644 --- a/src/main/java/com/rapidminer/operator/performance/BinaryClassificationPerformance.java +++ b/src/main/java/com/rapidminer/operator/performance/BinaryClassificationPerformance.java @@ -116,6 +116,11 @@ public class BinaryClassificationPerformance extends MeasuredPerformance { /** The weight attribute. Might be null. */ private Attribute weightAttribute; + /** + * True if the user defined positive class should be used instead of the label's default mapping. + */ + private boolean userDefinedPositiveClass = false; + public BinaryClassificationPerformance() { type = -1; } @@ -139,6 +144,7 @@ public BinaryClassificationPerformance(BinaryClassificationPerformance o) { } this.positiveClassName = o.positiveClassName; this.negativeClassName = o.negativeClassName; + this.userDefinedPositiveClass = o.userDefinedPositiveClass; } public BinaryClassificationPerformance(int type) { @@ -198,8 +204,8 @@ public void startCounting(ExampleSet eSet, boolean useExampleWeights) throws Ope throw new UserError(null, 157); } - this.negativeClassName = predictedLabelAttribute.getMapping().getNegativeString(); - this.positiveClassName = predictedLabelAttribute.getMapping().getPositiveString(); + updatePosNegClassNames(); + if (useExampleWeights) { this.weightAttribute = eSet.getAttributes().getWeight(); } @@ -209,9 +215,9 @@ public void startCounting(ExampleSet eSet, boolean useExampleWeights) throws Ope @Override public void countExample(Example example) { String labelString = example.getNominalValue(labelAttribute); - int label = predictedLabelAttribute.getMapping().getIndex(labelString); + int label = positiveClassName.equals(labelString) ? P : N; String predString = example.getNominalValue(predictedLabelAttribute); - int plabel = predictedLabelAttribute.getMapping().getIndex(predString); + int plabel = positiveClassName.equals(predString) ? P : N; double weight = 1.0d; if (weightAttribute != null) { @@ -432,4 +438,33 @@ public String getPositiveClassName() { public String getTitle() { return super.toString() + " (positive class: " + getPositiveClassName() + ")"; } + + /** + * Overrides the default positive class name with a user defined positive class name. If the argument is null it + * falls back to the default positive class name defined by the label's intern mapping. + * + * @param positiveClassName + * The positive class name or null to fall back to the default positive class name. + */ + public void setUserDefinedPositiveClassName(String positiveClassName) { + this.positiveClassName = positiveClassName; + userDefinedPositiveClass = positiveClassName != null; + } + + private void updatePosNegClassNames() throws UserError { + String mapNegativeClassName = predictedLabelAttribute.getMapping().getNegativeString(); + String mapPositiveClassName = predictedLabelAttribute.getMapping().getPositiveString(); + if (userDefinedPositiveClass) { + if (positiveClassName.equals(mapPositiveClassName)) { + negativeClassName = mapNegativeClassName; + } else if (positiveClassName.equals(mapNegativeClassName)) { + negativeClassName = mapPositiveClassName; + } else { + throw new UserError(null, "invalid_positive_class", positiveClassName); + } + } else { + positiveClassName = mapPositiveClassName; + negativeClassName = mapNegativeClassName; + } + } } diff --git a/src/main/java/com/rapidminer/operator/performance/BinominalClassificationPerformanceEvaluator.java b/src/main/java/com/rapidminer/operator/performance/BinominalClassificationPerformanceEvaluator.java index d884f6a34..a537aba9b 100644 --- a/src/main/java/com/rapidminer/operator/performance/BinominalClassificationPerformanceEvaluator.java +++ b/src/main/java/com/rapidminer/operator/performance/BinominalClassificationPerformanceEvaluator.java @@ -18,19 +18,30 @@ */ package com.rapidminer.operator.performance; +import static com.rapidminer.tools.FunctionWithThrowable.suppress; + +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; -import java.util.logging.Level; +import java.util.Optional; import com.rapidminer.example.Attribute; import com.rapidminer.example.ExampleSet; import com.rapidminer.example.Tools; +import com.rapidminer.example.table.NominalMapping; import com.rapidminer.operator.OperatorCapability; import com.rapidminer.operator.OperatorDescription; import com.rapidminer.operator.OperatorException; import com.rapidminer.operator.UserError; +import com.rapidminer.operator.ports.InputPort; +import com.rapidminer.operator.ports.metadata.AttributeMetaData; +import com.rapidminer.operator.ports.metadata.ExampleSetMetaData; +import com.rapidminer.parameter.ParameterType; +import com.rapidminer.parameter.ParameterTypeBoolean; +import com.rapidminer.parameter.ParameterTypeSuggestion; +import com.rapidminer.parameter.SuggestionProvider; import com.rapidminer.parameter.UndefinedParameterError; -import com.rapidminer.tools.LogService; +import com.rapidminer.parameter.conditions.BooleanParameterCondition; import com.rapidminer.tools.Ontology; @@ -68,78 +79,60 @@ */ public class BinominalClassificationPerformanceEvaluator extends AbstractPerformanceEvaluator { - /** The proper criteria to the names. */ - private static final Class[] SIMPLE_CRITERIA_CLASSES = { - com.rapidminer.operator.performance.AreaUnderCurve.Optimistic.class, - com.rapidminer.operator.performance.AreaUnderCurve.Optimistic.class, - com.rapidminer.operator.performance.AreaUnderCurve.Neutral.class, - com.rapidminer.operator.performance.AreaUnderCurve.Pessimistic.class }; + /** + * Iff this checkbox is {@code true}, the positive class parameter is shown to the user. + */ + public static final String PARAMETER_POSITIVE_CLASS_CHECKBOX = "manually_set_positive_class"; - public BinominalClassificationPerformanceEvaluator(OperatorDescription description) { - super(description); - } + /** + * The user optionally can use this parameter to explicitly specify the positive class. + */ + public static final String PARAMETER_POSITIVE_CLASS = "positive_class"; - @Override - protected void checkCompatibility(ExampleSet exampleSet) throws OperatorException { - Tools.isNonEmpty(exampleSet); - Tools.hasNominalLabels(exampleSet, "the calculation of performance criteria for binominal classification tasks"); + /** + * The positive class parameter checkbox and the positive class parameter are added to the existing parameter types + * at this index. + */ + private static final int POSITIVE_CLASS_PARAMETER_INDEX = 0; - Attribute label = exampleSet.getAttributes().getLabel(); - if (label.getMapping().size() != 2) { - throw new UserError(this, 114, "the calculation of performance criteria for binominal classification tasks", - label.getName()); - } - } + private String positiveClassName; - /** Returns null. */ - @Override - protected double[] getClassWeights(Attribute label) throws UndefinedParameterError { - return null; + public BinominalClassificationPerformanceEvaluator(OperatorDescription description) { + super(description); + positiveClassName = null; } @Override public List getCriteria() { - List performanceCriteria = new LinkedList(); + List performanceCriteria = new LinkedList<>(); // standard classification measures for (int i = 0; i < MultiClassificationPerformance.NAMES.length; i++) { performanceCriteria.add(new MultiClassificationPerformance(i)); } - for (int i = 0; i < SIMPLE_CRITERIA_CLASSES.length; i++) { - try { - performanceCriteria.add((PerformanceCriterion) SIMPLE_CRITERIA_CLASSES[i].newInstance()); - } catch (InstantiationException e) { - // LogService.getGlobal().logError("Cannot instantiate " + - // SIMPLE_CRITERIA_CLASSES[i] + ". Skipping..."); - LogService - .getRoot() - .log(Level.SEVERE, - "com.rapidminer.operator.performance.BinominalClassificationPerformanceEvaluator.instantiating_simple_criteria_classes_error", - SIMPLE_CRITERIA_CLASSES[i]); - } catch (IllegalAccessException e) { - // LogService.getGlobal().logError("Cannot instantiate " + - // SIMPLE_CRITERIA_CLASSES[i] + ". Skipping..."); - LogService - .getRoot() - .log(Level.SEVERE, - "com.rapidminer.operator.performance.BinominalClassificationPerformanceEvaluator.instantiating_simple_criteria_classes_error", - SIMPLE_CRITERIA_CLASSES[i]); - } - } + // AUC + AreaUnderCurve aucOpt = new AreaUnderCurve.Optimistic(); + AreaUnderCurve auc = new AreaUnderCurve.Neutral(); + AreaUnderCurve aucPes = new AreaUnderCurve.Pessimistic(); + + aucOpt.setUserDefinedPositiveClassName(positiveClassName); + auc.setUserDefinedPositiveClassName(positiveClassName); + aucPes.setUserDefinedPositiveClassName(positiveClassName); + + performanceCriteria.add(aucOpt); + performanceCriteria.add(auc); + performanceCriteria.add(aucPes); // binary classification criteria for (int i = 0; i < BinaryClassificationPerformance.NAMES.length; i++) { - performanceCriteria.add(new BinaryClassificationPerformance(i)); + BinaryClassificationPerformance b = new BinaryClassificationPerformance(i); + b.setUserDefinedPositiveClassName(positiveClassName); + performanceCriteria.add(b); } return performanceCriteria; } - @Override - protected boolean canEvaluate(int valueType) { - return Ontology.ATTRIBUTE_VALUE_TYPE.isA(valueType, Ontology.BINOMINAL); - } - @Override public boolean supportsCapability(OperatorCapability capability) { switch (capability) { @@ -162,4 +155,79 @@ public boolean supportsCapability(OperatorCapability capability) { return false; } } + + @Override + public List getParameterTypes() { + List types = new ArrayList<>(super.getParameterTypes()); + ParameterType posClassType = createPositiveClassParameter(); + posClassType.registerDependencyCondition(new BooleanParameterCondition(this, + PARAMETER_POSITIVE_CLASS_CHECKBOX, true, true)); + types.add(POSITIVE_CLASS_PARAMETER_INDEX, posClassType); + types.add(POSITIVE_CLASS_PARAMETER_INDEX, new ParameterTypeBoolean(PARAMETER_POSITIVE_CLASS_CHECKBOX, + "Check this to manually specify the positive class.", false, false)); + return types; + } + + @Override + protected void checkCompatibility(ExampleSet exampleSet) throws OperatorException { + Tools.isNonEmpty(exampleSet); + Tools.hasNominalLabels(exampleSet, "the calculation of performance criteria for binominal classification tasks"); + + Attribute label = exampleSet.getAttributes().getLabel(); + NominalMapping mapping = label.getMapping(); + if (mapping.size() != 2) { + throw new UserError(this, 114, "the calculation of performance criteria for binominal classification tasks", + label.getName()); + } + + // check if there is a user specified positive class and if it is valid + if (getParameterAsBoolean(PARAMETER_POSITIVE_CLASS_CHECKBOX)) { + String posClass = Optional.of(getParameterAsString(PARAMETER_POSITIVE_CLASS)).filter(s -> !s.isEmpty()).orElse(null); + if (posClass == null || mapping.getIndex(posClass) == -1) { + throw new UserError(this, "invalid_positive_class", posClass); + } + } + } + + @Override + protected void init(ExampleSet exampleSet) { + super.init(exampleSet); + if (getParameterAsBoolean(PARAMETER_POSITIVE_CLASS_CHECKBOX)) { + positiveClassName = Optional.of(PARAMETER_POSITIVE_CLASS).map(suppress(this::getParameterAsString)) + .filter(s -> !s.isEmpty()).orElse(null); + } else { + positiveClassName = null; + } + } + + /** + * Returns null. + */ + @Override + protected double[] getClassWeights(Attribute label) throws UndefinedParameterError { + return null; + } + + @Override + protected boolean canEvaluate(int valueType) { + return Ontology.ATTRIBUTE_VALUE_TYPE.isA(valueType, Ontology.BINOMINAL); + } + + /** + * Creates the positive class parameter as {@link ParameterTypeSuggestion}. + */ + private ParameterType createPositiveClassParameter() { + InputPort in = getInputPorts().getPortByName(INPUT_PORT_LABELLED_DATA); + SuggestionProvider suggestionProvider = (op, pl) -> { + if (op != BinominalClassificationPerformanceEvaluator.this) { + return new ArrayList<>(); + } + return Optional.ofNullable(in).map(suppress(ip -> ip.getMetaData(ExampleSetMetaData.class))) + .map(ExampleSetMetaData::getLabelMetaData).filter(AttributeMetaData::isNominal) + .map(AttributeMetaData::getValueSet).filter(vs -> vs.size() == 2) + .map(ArrayList::new).orElse(new ArrayList<>()); + }; + return new ParameterTypeSuggestion(PARAMETER_POSITIVE_CLASS, "Please select the positive class.", + suggestionProvider, true); + } } diff --git a/src/main/java/com/rapidminer/operator/ports/DummyPortPairExtender.java b/src/main/java/com/rapidminer/operator/ports/DummyPortPairExtender.java index 6705bfc96..8a4c9d88f 100644 --- a/src/main/java/com/rapidminer/operator/ports/DummyPortPairExtender.java +++ b/src/main/java/com/rapidminer/operator/ports/DummyPortPairExtender.java @@ -18,6 +18,7 @@ */ package com.rapidminer.operator.ports; +import com.rapidminer.operator.Operator; import com.rapidminer.operator.ProcessSetupError.Severity; import com.rapidminer.operator.SimpleProcessSetupError; import com.rapidminer.operator.ports.metadata.MDTransformationRule; @@ -43,28 +44,30 @@ public DummyPortPairExtender(String name, InputPorts inPorts, OutputPorts outPor */ @Override public MDTransformationRule makePassThroughRule() { - return new MDTransformationRule() { - - @Override - public void transformMD() { - boolean somethingConnected = false; - for (PortPair pair : getManagedPairs()) { - // testing if connected for execution order - somethingConnected |= pair.getInputPort().isConnected() || pair.getOutputPort().isConnected(); - // transforming meta data. - MetaData inData = pair.getInputPort().getMetaData(); - if (inData != null) { - inData = transformMetaData(inData.clone()); - inData.addToHistory(pair.getOutputPort()); - pair.getOutputPort().deliverMD(inData); - } else { - pair.getOutputPort().deliverMD(null); - } + return () -> { + boolean somethingConnected = false; + for (PortPair pair : getManagedPairs()) { + // testing if connected for execution order + somethingConnected |= pair.getInputPort().isConnected() || pair.getOutputPort().isConnected(); + // transforming meta data. + MetaData inData = pair.getInputPort().getMetaData(); + if (inData != null) { + inData = transformMetaData(inData.clone()); + inData.addToHistory(pair.getOutputPort()); + pair.getOutputPort().deliverMD(inData); + } else { + pair.getOutputPort().deliverMD(null); } - if (!somethingConnected) { - PortOwner owner = getManagedPairs().get(0).getInputPort().getPorts().getOwner(); - owner.getOperator().addError( - new SimpleProcessSetupError(Severity.WARNING, owner, "execution_order_undefined")); + } + if (!somethingConnected) { + PortOwner owner = getManagedPairs().get(0).getInputPort().getPorts().getOwner(); + Operator operator = owner.getOperator(); + // check all other ports, too + InputPorts inputPorts = operator.getInputPorts(); + OutputPorts outputPorts = operator.getOutputPorts(); + if ((inputPorts.getNumberOfPorts() == getManagedPairs().size() || inputPorts.getNumberOfConnectedPorts() == 0) + && (outputPorts.getNumberOfPorts() == getManagedPairs().size() || outputPorts.getNumberOfConnectedPorts() == 0)) { + operator.addError(new SimpleProcessSetupError(Severity.WARNING, owner, "execution_order_undefined")); } } }; diff --git a/src/main/java/com/rapidminer/operator/ports/Port.java b/src/main/java/com/rapidminer/operator/ports/Port.java index 1eda1727a..4089af375 100644 --- a/src/main/java/com/rapidminer/operator/ports/Port.java +++ b/src/main/java/com/rapidminer/operator/ports/Port.java @@ -80,8 +80,10 @@ public interface Port extends Observable { /** * This method returns the object of the desired class or throws an UserError if no object is - * present or cannot be casted to the desiredClass. * @throws UserError if data is missing or of - * wrong class. + * present or cannot be casted to the desiredClass. + * + * @throws UserError + * if data is missing or of wrong class. */ public T getData(Class desiredClass) throws UserError; diff --git a/src/main/java/com/rapidminer/operator/ports/PortPairExtender.java b/src/main/java/com/rapidminer/operator/ports/PortPairExtender.java index 5ffb6cf9f..365c73335 100644 --- a/src/main/java/com/rapidminer/operator/ports/PortPairExtender.java +++ b/src/main/java/com/rapidminer/operator/ports/PortPairExtender.java @@ -18,13 +18,18 @@ */ package com.rapidminer.operator.ports; +import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import com.rapidminer.adaption.belt.AtPortConverter; +import com.rapidminer.gui.renderer.RendererService; import com.rapidminer.operator.IOObject; +import com.rapidminer.operator.IOObjectCollection; import com.rapidminer.operator.UserError; +import com.rapidminer.operator.ports.metadata.CollectionPrecondition; import com.rapidminer.operator.ports.metadata.MDTransformationRule; import com.rapidminer.operator.ports.metadata.MetaData; import com.rapidminer.operator.ports.metadata.SimplePrecondition; @@ -37,6 +42,11 @@ * Operators probably want to connect these ports by a * {@link com.rapidminer.operator.ports.metadata.ManyToManyPassThroughRule}. It guarantees that * there is always exactly one pair of in and output pairs which is not connected. + *

      + * Via the different constructors input ports can be customized to accept any input, + * input of a certain class or multiple inputs of a certain class contained inside of an {@link IOObjectCollection}. + *

      + * In case of multiple inputs per port please use the method {@link #getData(Class, boolean)} to retrieve the data. * * @see PortPairExtender * @see MultiPortPairExtender @@ -45,13 +55,28 @@ */ public class PortPairExtender implements PortExtender { + /** + * Name prefix for the input ports (and output ports if no output port name has been specified) + */ private final String name; + /** + * Name prefix for the output ports + */ + private final String outName; + private final InputPorts inPorts; private final OutputPorts outPorts; - private final List managedPairs = new LinkedList(); + private final List managedPairs = new LinkedList<>(); - /** If non null, add this meta data as a SimplePrecondition to each generated input port. */ + /** + * If not {@code null}, add this meta data as a SimplePrecondition or {@link CollectionPrecondition} to each generated input + * port. + */ private final MetaData preconditionMetaData; + /** + * If {@code true}, the PortPairExtender will use a {@link CollectionPrecondition} instead of a {@link SimplePrecondition}. + */ + private boolean allowCollection; private boolean isChanging = false; @@ -92,19 +117,65 @@ public PortPairExtender(String name, InputPorts inPorts, OutputPorts outPorts) { } /** - * Creates a new port pair extender + * Creates a new port pair extender. * * @param name - * The name prefix for all generated ports. + * The name prefix for all generated ports. Must not be {@code null}. * @param inPorts - * Add generated input ports to these InputPorts + * Add generated input ports to these InputPorts. Must not be {@code null}. * @param outPorts - * Add generated output ports to these OutputPorts + * Add generated output ports to these OutputPorts. Must not be {@code null}. * @param preconditionMetaData - * If non-null, create a SimplePrecondition for each newly generated input port. + * If not {@code null}, creates a {@link SimplePrecondition} or {@link CollectionPrecondition} for each + * newly generated input port. If {@code null}, no preconditions will be added to the ports. */ public PortPairExtender(String name, InputPorts inPorts, OutputPorts outPorts, MetaData preconditionMetaData) { + this(name, inPorts, outPorts, preconditionMetaData, false); + } + + /** + * Creates a new port pair extender. + * + * @param name + * The name prefix for all generated ports. Must not be {@code null}. + * @param inPorts + * Add generated input ports to these InputPorts. Must not be {@code null}. + * @param outPorts + * Add generated output ports to these OutputPorts. Must not be {@code null}. + * @param preconditionMetaData + * If not {@code null}, creates a {@link SimplePrecondition} or {@link CollectionPrecondition} for each newly + * generated input port. If {@code null}, no preconditions will be added to the ports. + * @param allowCollection + * If {@code true} a CollectionPrecondition a {@link CollectionPrecondition} is used for each newly generated port + * to optionally allow {@link IOObjectCollection} as input. + */ + public PortPairExtender(String name, InputPorts inPorts, OutputPorts outPorts, MetaData preconditionMetaData, boolean allowCollection) { + this(name, null, inPorts, outPorts, preconditionMetaData, allowCollection); + } + + /** + * Creates a new port pair extender. + * + * @param name + * The name prefix for all the input ports. Must not be {@code null}. + * @param outName + * The name prefix for all the ouput ports. If {@code null}, the {@code name} parameter will be reused instead. + * @param inPorts + * Add generated input ports to these InputPorts. Must not be {@code null}. + * @param outPorts + * Add generated output ports to these OutputPorts. Must not be {@code null}. + * @param preconditionMetaData + * If not {@code null}, creates a {@link SimplePrecondition} or {@link CollectionPrecondition} for each newly + * generated input port. If {@code null}, no preconditions will be added to the ports. + * @param allowCollection + * If {@code true} a {@link CollectionPrecondition} is used for each newly generated + * port to optionally allow {@link IOObjectCollection} as input. + */ + public PortPairExtender(String name, String outName, InputPorts inPorts, OutputPorts outPorts, + MetaData preconditionMetaData, boolean allowCollection) { + this.allowCollection = allowCollection; this.name = name; + this.outName = outName != null ? outName : name; this.inPorts = inPorts; this.outPorts = outPorts; this.preconditionMetaData = preconditionMetaData; @@ -163,9 +234,14 @@ private PortPair createPort() { runningId++; InputPort in = inPorts.createPassThroughPort(name + " " + runningId); if (preconditionMetaData != null) { - in.addPrecondition(new SimplePrecondition(in, preconditionMetaData, false)); + SimplePrecondition sp = new SimplePrecondition(in, preconditionMetaData, false); + if (allowCollection) { + in.addPrecondition(new CollectionPrecondition(sp)); + } else { + in.addPrecondition(sp); + } } - OutputPort out = outPorts.createPassThroughPort(name + " " + runningId); + OutputPort out = outPorts.createPassThroughPort(outName + " " + runningId); return new PortPair(in, out); } @@ -177,18 +253,18 @@ private void deletePorts(PortPair pair) { outPorts.removePort(pair.outputPort); } - private void fixNames() { + protected void fixNames() { runningId = 0; for (PortPair pair : managedPairs) { runningId++; inPorts.renamePort(pair.inputPort, name + "_tmp_" + runningId); - outPorts.renamePort(pair.outputPort, name + "_tmp_" + runningId); + outPorts.renamePort(pair.outputPort, outName + "_tmp_" + runningId); } runningId = 0; for (PortPair pair : managedPairs) { runningId++; inPorts.renamePort(pair.inputPort, name + " " + runningId); - outPorts.renamePort(pair.outputPort, name + " " + runningId); + outPorts.renamePort(pair.outputPort, outName + " " + runningId); } } @@ -245,7 +321,7 @@ public List getManagedPairs() { } /** - * Returns a list of all non-null data delivered to the input ports created by this port + * Returns a list of all non-{@code null} data delivered to the input ports created by this port * extender. * * @throws UserError @@ -263,10 +339,21 @@ public List getData() throws UserError { return results; } + /** + * Returns a list of all non-{@code null} data delivered to the input ports created by this port extender and casts + * the data to the desired class. + * + * @param desiredClass + * The class the data should be casted to. + * @return Non-{@code null} data delivered to the output ports created by this port extender. If there is nc data + * the List will be empty but never {@code null}. + * @throws UserError + * If data is not of the requested type. + */ public List getData(Class desiredClass) throws UserError { - List results = new LinkedList(); + List results = new LinkedList<>(); for (PortPair pair : managedPairs) { - T data = pair.inputPort. getDataOrNull(desiredClass); + T data = pair.inputPort.getDataOrNull(desiredClass); if (data != null) { results.add(data); } @@ -275,7 +362,7 @@ public List getData(Class desiredClass) throws UserEr } /** - * Returns a list of all non-null data delivered to the input ports created by this port + * Returns a list of all non-{@code null} data delivered to the output ports created by this port * extender. * * @throws UserError @@ -283,7 +370,7 @@ public List getData(Class desiredClass) throws UserEr */ @Deprecated public List getOutputData() throws UserError { - List results = new LinkedList(); + List results = new LinkedList<>(); for (PortPair pair : managedPairs) { T data = pair.outputPort. getDataOrNull(); if (data != null) { @@ -293,8 +380,19 @@ public List getOutputData() throws UserError { return results; } + /** + * Returns a list of all non-{@code null} data delivered to the output ports created by this port extender and casts + * the data to the desired class. + * + * @param desiredClass + * The class the data should be casted to. + * @return Non-{@code null} data delivered to the output ports created by this port extender. If there is nc data + * the List will be empty but never {@code null}. + * @throws UserError + * If data is not of the requested type. + */ public List getOutputData(Class desiredClass) throws UserError { - List results = new LinkedList(); + List results = new LinkedList<>(); for (PortPair pair : managedPairs) { T data = pair.outputPort. getDataOrNull(desiredClass); if (data != null) { @@ -308,7 +406,7 @@ public List getOutputData(Class desiredClass) throws * This method is a convenient method for delivering several IOObjects. But keep in mind that * you cannot deliver more IObjects than you received first hand. First objects in list will be * delivered on the first port. If input ports are not connected or got not delivered an objects - * unequal null, the corresponding output port is skipped. + * unequal {@code null}, the corresponding output port is skipped. */ public void deliver(List ioObjectList) { Iterator portIterator = getManagedPairs().iterator(); @@ -341,4 +439,68 @@ public void ensureMinimumNumberOfPorts(int minNumber) { this.minNumber = minNumber; updatePorts(); } + + /** + * Returns a list of non-{@code null} data of all input ports. + * + * @param unfold + * If {@code true}, collections are added as individual objects rather than as a collection. + * The unfolding is done recursively. + * @throws UserError + */ + @SuppressWarnings("unchecked") + public List getData(Class desiredClass, boolean unfold) throws UserError { + List results = new ArrayList<>(); + for (PortPair port : managedPairs) { + IOObject data = port.getInputPort().getAnyDataOrNull(); + if (data != null) { + if (unfold && data instanceof IOObjectCollection) { + unfold((IOObjectCollection) data, results, desiredClass, port); + } else { + addSingle(results, data, desiredClass, port); + } + } + } + return results; + } + + /** + * Unfolds the given IOObjectCollection recursively. + * + * @param desiredClass + * method will throw unless all non-collection children are of type desired class + * @param port + * Used for error message only + */ + @SuppressWarnings("unchecked") + private void unfold(IOObjectCollection collection, List results, Class desiredClass, + PortPair port) throws UserError { + for (IOObject obj : collection.getObjects()) { + if (obj instanceof IOObjectCollection) { + unfold((IOObjectCollection) obj, results, desiredClass, port); + } else { + addSingle(results, obj, desiredClass, port); + } + } + } + + /** + * Adds the data to the results list if it is of the desired class or convertible to it. Throws an user error + * otherwise. + */ + private void addSingle(List results, IOObject data, Class desiredClass, PortPair port) + throws UserError { + if (desiredClass.isInstance(data)) { + results.add(desiredClass.cast(data)); + } else if (AtPortConverter.isConvertible(data.getClass(), desiredClass)) { + results.add(desiredClass.cast(AtPortConverter.convert(data, port.getInputPort()))); + } else { + throw new UserError(inPorts.getOwner().getOperator(), 156, + RendererService.getName(data.getClass()), port.getInputPort().getName(), + RendererService.getName(desiredClass)); + } + } + + + } diff --git a/src/main/java/com/rapidminer/operator/ports/impl/AbstractPort.java b/src/main/java/com/rapidminer/operator/ports/impl/AbstractPort.java index ec2b6b084..6eedf207a 100644 --- a/src/main/java/com/rapidminer/operator/ports/impl/AbstractPort.java +++ b/src/main/java/com/rapidminer/operator/ports/impl/AbstractPort.java @@ -112,27 +112,33 @@ public IOObject getAnyDataOrNull() { @Override public T getData(Class desiredClass) throws UserError { - IOObject data = getAnyDataOrNull(); - if (data == null) { - throw new PortUserError(this, 149, getSpec() + (isConnected() ? " (connected)" : " (disconnected)")); - } else if (desiredClass.isAssignableFrom(data.getClass())) { - return desiredClass.cast(data); - } else if (AtPortConverter.isConvertible(data.getClass(), desiredClass)) { - return desiredClass.cast(AtPortConverter.convert(data, this)); - } else { - PortUserError error = new PortUserError(this, 156, RendererService.getName(data.getClass()), this.getName(), - RendererService.getName(desiredClass)); - error.setExpectedType(desiredClass); - error.setActualType(data.getClass()); - throw error; - } + return getData(desiredClass, false); } @Override public T getDataOrNull(Class desiredClass) throws UserError { + return getData(desiredClass, true); + } + + /** + * This method returns the object of the desired class or throws an {@link UserError} if object cannot be cast to the desiredClass. + * Dependening on allowNull either returns a {@code null} value or throws a {@link UserError} + * + * @param desiredClass + * the super class of desired type of data + * @param allowNull + * if {@code null} value should be returned or throw an error + * @throws UserError + * if an error occurs + * @since 9.3 + */ + private T getData(Class desiredClass, boolean allowNull) throws UserError { IOObject data = getAnyDataOrNull(); if (data == null) { - return null; + if (allowNull) { + return null; + } + throw new PortUserError(this, 149, getSpec() + (isConnected() ? " (connected)" : " (disconnected)")); } else if (desiredClass.isAssignableFrom(data.getClass())) { return desiredClass.cast(data); } else if (AtPortConverter.isConvertible(data.getClass(), desiredClass)) { @@ -146,8 +152,6 @@ public T getDataOrNull(Class desiredClass) throws UserEr } } - - @SuppressWarnings("unchecked") @Deprecated @Override diff --git a/src/main/java/com/rapidminer/operator/ports/metadata/ConnectionInformationMetaData.java b/src/main/java/com/rapidminer/operator/ports/metadata/ConnectionInformationMetaData.java new file mode 100644 index 000000000..0cc2d7cc8 --- /dev/null +++ b/src/main/java/com/rapidminer/operator/ports/metadata/ConnectionInformationMetaData.java @@ -0,0 +1,188 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.operator.ports.metadata; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Collections; +import java.util.Objects; +import java.util.Optional; + +import org.apache.commons.lang.StringUtils; + +import com.rapidminer.connection.ConnectionInformationContainerIOObject; +import com.rapidminer.connection.ConnectionInformationSerializer; +import com.rapidminer.connection.configuration.ConnectionConfiguration; +import com.rapidminer.connection.configuration.ConnectionConfigurationImpl; +import com.rapidminer.connection.util.ConnectionI18N; +import com.rapidminer.tools.I18N; + + +/** + * MetaData object for {@link com.rapidminer.connection.ConnectionInformation} contains the complete configuration. + * + * @author Andreas Timm + * @since 9.2 + */ +public class ConnectionInformationMetaData extends MetaData { + + /** + * Tag delimiter + */ + private static final String TAG_DELIMITER = ", "; + /** + * Maximum description length, about four lines + */ + static final int DESCRIPTION_PREVIEW_LENGTH = 300; + /** + * used if no configuration is available + */ + static final ConnectionConfiguration UNKNOWN_CONNECTION = new ConnectionConfigurationImpl(I18N.getGUILabel("connection.type.unknown.label"), "unknown"); + + // Use this configuration object to show its content as metadata + private ConnectionConfiguration configuration; + + /** + * Constructor required by {@link MetaData#clone()} + */ + public ConnectionInformationMetaData() { + super(ConnectionInformationContainerIOObject.class); + } + + /** + * Create a new {@link ConnectionInformationMetaData} instance with the given {@link ConnectionConfiguration} to + * show as its content + * + * @param connectionConfiguration + * will be kept as a reference to show its content + */ + public ConnectionInformationMetaData(ConnectionConfiguration connectionConfiguration) { + this(); + configuration = connectionConfiguration; + } + + /** + * Create a new {@link ConnectionInformationMetaData} instance with the given {@link ConnectionConfiguration} to + * show as its content + * + * @param object + * will be kept as a reference to show its content + * @param ignored + * not used + * @see MetaDataFactory#registerIOObjectMetaData(Class, Class) + */ + public ConnectionInformationMetaData(ConnectionInformationContainerIOObject object, boolean ignored) { + this(object.getConnectionInformation().getConfiguration()); + } + + /** + * Returns the type of the connection. Might return {@code null}. + */ + public String getConnectionType() { + return configuration == null ? null : configuration.getType(); + } + + @Override + public String getDescription() { + final StringBuilder builder = new StringBuilder(super.getDescription()); + ConnectionConfiguration config = Optional.ofNullable(getConfiguration()).orElse(UNKNOWN_CONNECTION); + String tags = String.join(TAG_DELIMITER, Optional.ofNullable(config.getTags()).orElseGet(Collections::emptyList)).trim(); + String description = StringUtils.abbreviate(Objects.toString(config.getDescription(), "").trim(), DESCRIPTION_PREVIEW_LENGTH); + + if (!description.isEmpty()) { + builder.append("

      ").append(description).append("

      "); + } + + if (!tags.isEmpty()) { + builder.append("

      "); + builder.append(I18N.getGUILabel("connection.metadata.connection_type.tags.label")).append(" "); + builder.append(tags).append("

      "); + } + + return builder.toString(); + } + + @Override + protected String getTitleForDescription() { + final StringBuilder builder = new StringBuilder(); + ConnectionConfiguration config = Optional.ofNullable(getConfiguration()).orElse(UNKNOWN_CONNECTION); + // Prevent the enclosing

      tag in repository view from being empty (= line break) + builder.append(""); + builder.append(""); + builder.append(""); + builder.append(""); + builder.append("
      ").append(""); + builder.append(I18N.getGUIMessage("gui.label.connection.metadata.connection_type.message", "" + config.getName(), "" + ConnectionI18N.getTypeName(config.getType()))); + builder.append("
      "); + return builder.toString(); + } + + /** + * Required for Java serialization, this is a special @Override style.. + * + * @param out + * writing to this stream + * @throws IOException + * could not write + */ + private void writeObject(ObjectOutputStream out) throws IOException { + ConnectionInformationSerializer.LOCAL.writeJson(out, configuration); + } + + /** + * Required for Java serialization, this is a special @Override style.. + * + * @param in + * reading from this stream + * @throws IOException + * could not read + */ + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + configuration = ConnectionInformationSerializer.LOCAL.loadConfiguration(in); + } + + @Override + public ConnectionInformationMetaData clone() { + ConnectionInformationMetaData cimdClone = (ConnectionInformationMetaData) super.clone(); + if (configuration == null) { + return cimdClone; + } + try { + // keep only a copy of the configuration + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ConnectionInformationSerializer.LOCAL.writeJson(baos, configuration); + cimdClone.configuration = ConnectionInformationSerializer.LOCAL.loadConfiguration(new ByteArrayInputStream(baos.toByteArray())); + } catch (IOException e) { + throw new RuntimeException("Cloning the connection configuration failed", e); + } + return cimdClone; + } + + /** + * Gets the connection configuration + * + * @return the configuration object + */ + public ConnectionConfiguration getConfiguration() { + return configuration; + } +} diff --git a/src/main/java/com/rapidminer/operator/ports/metadata/MDTransformer.java b/src/main/java/com/rapidminer/operator/ports/metadata/MDTransformer.java index a2c0d6fa4..4284b6825 100644 --- a/src/main/java/com/rapidminer/operator/ports/metadata/MDTransformer.java +++ b/src/main/java/com/rapidminer/operator/ports/metadata/MDTransformer.java @@ -18,6 +18,7 @@ */ package com.rapidminer.operator.ports.metadata; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -44,7 +45,7 @@ */ public class MDTransformer { - private final LinkedList transformationRules = new LinkedList(); + private final LinkedList transformationRules = new LinkedList<>(); private final Operator operator; public MDTransformer(Operator op) { @@ -53,7 +54,7 @@ public MDTransformer(Operator op) { /** Executes all rules added by {@link #addRule}. */ public void transformMetaData() { - for (MDTransformationRule rule : transformationRules) { + for (MDTransformationRule rule : new ArrayList<>(transformationRules)) { try { rule.transformMD(); } catch (Exception e) { diff --git a/src/main/java/com/rapidminer/operator/ports/metadata/MetaData.java b/src/main/java/com/rapidminer/operator/ports/metadata/MetaData.java index fa1b25cec..43a630dc4 100644 --- a/src/main/java/com/rapidminer/operator/ports/metadata/MetaData.java +++ b/src/main/java/com/rapidminer/operator/ports/metadata/MetaData.java @@ -166,7 +166,7 @@ public String toString() { } public String getDescription() { - String name = RendererService.getName(dataClass); + String name = getTitleForDescription(); if (name == null) { name = dataClass.getSimpleName(); } @@ -185,6 +185,17 @@ public String getDescription() { return desc.toString(); } + /** + * Returns the title that is used in the {@link #getDescription()} method + *

      The default implementation checks {@link RendererService#getName}

      + *

      If this method returns {@code null}, the {@link Class#getSimpleName()} of the data class is used.

      + * + * @return the description title, might contain html + */ + protected String getTitleForDescription() { + return RendererService.getName(dataClass); + } + /** * Returns true if isData is compatible with this meta data, where this represents * desired meta data and isData represents meta data that was actually delivered. diff --git a/src/main/java/com/rapidminer/operator/ports/metadata/MetaDataFactory.java b/src/main/java/com/rapidminer/operator/ports/metadata/MetaDataFactory.java index dbf1cedbd..8e541322e 100644 --- a/src/main/java/com/rapidminer/operator/ports/metadata/MetaDataFactory.java +++ b/src/main/java/com/rapidminer/operator/ports/metadata/MetaDataFactory.java @@ -1,21 +1,21 @@ /** * Copyright (C) 2001-2019 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.operator.ports.metadata; import com.rapidminer.adaption.belt.IOTable; @@ -24,6 +24,7 @@ import com.rapidminer.operator.IOObject; import com.rapidminer.operator.IOObjectCollection; import com.rapidminer.operator.Model; +import com.rapidminer.connection.ConnectionInformationContainerIOObject; import com.rapidminer.tools.DominatingClassFinder; import com.rapidminer.tools.LogService; @@ -34,12 +35,12 @@ /** * Factory class for creating {@link MetaData} objects for given {@link IOObject}s. - * + * * See {@link #registerIOObjectMetaData(Class, Class)} and * {@link #createMetaDataforIOObject(IOObject, boolean)} for a description. - * + * * @author Nils Woehler - * + * */ public class MetaDataFactory { @@ -60,6 +61,7 @@ private MetaDataFactory() { MetaDataFactory.registerIOObjectMetaData(IOTable.class, ExampleSetMetaData.class); MetaDataFactory.registerIOObjectMetaData(IOObjectCollection.class, CollectionMetaData.class); MetaDataFactory.registerIOObjectMetaData(Model.class, ModelMetaData.class); + MetaDataFactory.registerIOObjectMetaData(ConnectionInformationContainerIOObject.class, ConnectionInformationMetaData.class); } /** @@ -70,7 +72,7 @@ private MetaDataFactory() { * clutter their visual representation). */ public static void registerIOObjectMetaData(Class ioObjectClass, - Class metaDataClass) { + Class metaDataClass) { try { metaDataClass.getConstructor(ioObjectClass, boolean.class); } catch (Throwable e) { @@ -107,7 +109,7 @@ public MetaData createMetaDataforIOObject(IOObject ioo, boolean shortened) { } private MetaData instantiateMetaData(Class metaDataClass, - Class compatibleIOObjectClass, IOObject ioo, boolean shortened) { + Class compatibleIOObjectClass, IOObject ioo, boolean shortened) { if (metaDataClass != null) { try { return metaDataClass.getConstructor(compatibleIOObjectClass, boolean.class).newInstance(ioo, shortened); diff --git a/src/main/java/com/rapidminer/operator/ports/quickfix/ParameterSettingQuickFix.java b/src/main/java/com/rapidminer/operator/ports/quickfix/ParameterSettingQuickFix.java index 1998d194b..66c37b02f 100644 --- a/src/main/java/com/rapidminer/operator/ports/quickfix/ParameterSettingQuickFix.java +++ b/src/main/java/com/rapidminer/operator/ports/quickfix/ParameterSettingQuickFix.java @@ -63,16 +63,14 @@ public ParameterSettingQuickFix(Operator operator, String parameterName, String seti18nKey("correct_parameter_settings_with_wizard"); } else if (type instanceof ParameterTypeList) { seti18nKey("correct_parameter_settings_list", parameterName.replace('_', ' ')); - } - - if (value != null) { - if (type instanceof ParameterTypeBoolean) { - if (value.equals("true")) { - seti18nKey("correct_parameter_settings_boolean_enable", parameterName.replace('_', ' ')); - } else { - seti18nKey("correct_parameter_settings_boolean_disable", parameterName.replace('_', ' ')); - } + } else if (value != null && type instanceof ParameterTypeBoolean) { + String i18nKey; + if (Boolean.parseBoolean(value)) { + i18nKey = "correct_parameter_settings_boolean_enable"; + } else { + i18nKey = "correct_parameter_settings_boolean_disable"; } + seti18nKey(i18nKey, parameterName.replace('_', ' ')); } } diff --git a/src/main/java/com/rapidminer/operator/preprocessing/PreprocessingOperator.java b/src/main/java/com/rapidminer/operator/preprocessing/PreprocessingOperator.java index e054d8201..306c7753a 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/PreprocessingOperator.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/PreprocessingOperator.java @@ -101,7 +101,10 @@ protected ExampleSetMetaData modifyMetaData(ExampleSetMetaData exampleSetMetaDat if (replacement != null) { if (replacement.size() == 1) { AttributeMetaData replacementAttribute = replacement.iterator().next(); - replacementAttribute.setRole(exampleSetMetaData.getAttributeByName(amd.getName()).getRole()); + if(replacementAttribute.getName().equals(amd.getName())){ + // In this case, the variable most likely remained the same. Therefore, we preserve its role. + replacementAttribute.setRole(exampleSetMetaData.getAttributeByName(amd.getName()).getRole()); + } } exampleSetMetaData.removeAttribute(amd); exampleSetMetaData.addAllAttributes(replacement); diff --git a/src/main/java/com/rapidminer/operator/preprocessing/RemoveUnusedNominalValuesOperator.java b/src/main/java/com/rapidminer/operator/preprocessing/RemoveUnusedNominalValuesOperator.java index 170160505..c97b9dd2e 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/RemoveUnusedNominalValuesOperator.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/RemoveUnusedNominalValuesOperator.java @@ -58,6 +58,13 @@ public class RemoveUnusedNominalValuesOperator extends PreprocessingOperator { */ private static final OperatorVersion VERSION_MAY_WRITE_INTO_DATA = new OperatorVersion(7, 1, 1); + /** + * Versions <= this version did not sort the internal mapping if an attribute did not have any unused values + * regardless of the sort alphabetically parameter. + */ + private static final OperatorVersion VERSION_DOESNT_SORT_IF_NO_UNUSED_VARIABLES + = new OperatorVersion(9, 2, 1); + public RemoveUnusedNominalValuesOperator(OperatorDescription description) { super(description); } @@ -73,7 +80,7 @@ protected Collection modifyAttributeMetaData(ExampleSetMetaDa public PreprocessingModel createPreprocessingModel(ExampleSet exampleSet) throws OperatorException { boolean sortMappings = getParameterAsBoolean(PARAMETER_SORT_MAPPING_ALPHABETICALLY); - Map translations = new HashMap(); + Map translations = new HashMap<>(); exampleSet.recalculateAllAttributeStatistics(); for (Attribute attribute : exampleSet.getAttributes()) { @@ -86,12 +93,14 @@ public PreprocessingModel createPreprocessingModel(ExampleSet exampleSet) throws translation.newMapping.mapString(value); } } - if (translation.newMapping.size() < attribute.getMapping().size()) { + if (translation.newMapping.size() < attribute.getMapping().size() || + (sortMappings && getCompatibilityLevel().isAbove(VERSION_DOESNT_SORT_IF_NO_UNUSED_VARIABLES))) { if (sortMappings) { translation.newMapping.sortMappings(); } translations.put(attribute.getName(), translation); } + // else: don't need a translation as nothing is changed } } return new RemoveUnusedNominalValuesModel(exampleSet, translations); @@ -130,6 +139,6 @@ public boolean writesIntoExistingData() { @Override public OperatorVersion[] getIncompatibleVersionChanges() { return (OperatorVersion[]) ArrayUtils.addAll(super.getIncompatibleVersionChanges(), - new OperatorVersion[] { VERSION_MAY_WRITE_INTO_DATA }); + new OperatorVersion[]{VERSION_MAY_WRITE_INTO_DATA, VERSION_DOESNT_SORT_IF_NO_UNUSED_VARIABLES }); } } diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueMapper.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueMapper.java index 96d9a4654..41b2f55ef 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueMapper.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueMapper.java @@ -46,6 +46,7 @@ import com.rapidminer.parameter.ParameterType; import com.rapidminer.parameter.ParameterTypeBoolean; import com.rapidminer.parameter.ParameterTypeList; +import com.rapidminer.parameter.ParameterTypeRegexp; import com.rapidminer.parameter.ParameterTypeString; import com.rapidminer.parameter.UndefinedParameterError; import com.rapidminer.parameter.conditions.BooleanParameterCondition; @@ -96,11 +97,21 @@ public class AttributeValueMapper extends AbstractValueProcessing { /** The parameter name for "The second value which should be merged." */ public static final String PARAMETER_OLD_VALUES = "old_values"; - /** The parameter name for "All occurrences of this value will be replaced." */ - public static final String PARAMETER_REPLACE_WHAT = "replace_what"; + /** + * The parameter name for "All occurrences of this value will be replaced." + * + * @deprecated since 9.3; use {@link ParameterTypeRegexp#PARAMETER_REPLACE_WHAT} instead + */ + @Deprecated + public static final String PARAMETER_REPLACE_WHAT = ParameterTypeRegexp.PARAMETER_REPLACE_WHAT; - /** The parameter name for "The new attribute value to use." */ - public static final String PARAMETER_REPLACE_BY = "replace_by"; + /** + * The parameter name for "The new attribute value to use." + * + * @deprecated since 9.3; use {@link ParameterTypeRegexp#PARAMETER_REPLACE_BY} instead + */ + @Deprecated + public static final String PARAMETER_REPLACE_BY = ParameterTypeRegexp.PARAMETER_REPLACE_BY; /** * The parameter name for "Enables matching based on regular expressions; original values @@ -152,8 +163,8 @@ public ExampleSetMetaData applyOnFilteredMetaData(ExampleSetMetaData emd) { boolean useValueRegex = getParameterAsBoolean(PARAMETER_CONSIDER_REGULAR_EXPRESSIONS); List mappingParameterList = getParameterList(PARAMETER_VALUE_MAPPINGS); - String replaceWhat = getParameterAsString(PARAMETER_REPLACE_WHAT); - String replaceBy = getParameterAsString(PARAMETER_REPLACE_BY); + String replaceWhat = getParameterAsString(ParameterTypeRegexp.PARAMETER_REPLACE_WHAT); + String replaceBy = getParameterAsString(ParameterTypeRegexp.PARAMETER_REPLACE_BY); HashMap mappings = new LinkedHashMap(); HashMap patternMappings = new LinkedHashMap(); if (replaceWhat != null && replaceBy != null && !replaceWhat.equals("") && !replaceBy.equals("")) { @@ -294,8 +305,8 @@ public ExampleSet applyOnFiltered(ExampleSet exampleSet) throws OperatorExceptio boolean useValueRegex = getParameterAsBoolean(PARAMETER_CONSIDER_REGULAR_EXPRESSIONS); List mappingParameterList = getParameterList(PARAMETER_VALUE_MAPPINGS); - String replaceWhat = getParameterAsString(PARAMETER_REPLACE_WHAT); - String replaceBy = getParameterAsString(PARAMETER_REPLACE_BY); + String replaceWhat = getParameterAsString(ParameterTypeRegexp.PARAMETER_REPLACE_WHAT); + String replaceBy = getParameterAsString(ParameterTypeRegexp.PARAMETER_REPLACE_BY); HashMap mappings = new LinkedHashMap(); HashMap patternMappings = new LinkedHashMap(); if (replaceWhat != null && replaceBy != null && !replaceWhat.equals("") && !replaceBy.equals("")) { @@ -458,13 +469,13 @@ public List getParameterTypes() { type.setPrimary(true); types.add(type); - type = new ParameterTypeString(PARAMETER_REPLACE_WHAT, "All occurrences of this value will be replaced.", true); - type.setExpert(false); - types.add(type); + ParameterTypeRegexp regexp = new ParameterTypeRegexp(ParameterTypeRegexp.PARAMETER_REPLACE_WHAT, "All occurrences of this value will be replaced.", true, false); + types.add(regexp); - type = new ParameterTypeString(PARAMETER_REPLACE_BY, "The new attribute value to use.", true); - type.setExpert(false); - types.add(type); + ParameterTypeString replacement = new ParameterTypeString(ParameterTypeRegexp.PARAMETER_REPLACE_BY, "The new attribute value to use.", true); + regexp.setReplacementParameter(replacement); + replacement.setExpert(false); + types.add(replacement); types.add(new ParameterTypeBoolean(PARAMETER_CONSIDER_REGULAR_EXPRESSIONS, "Enables matching based on regular expressions; original values may be specified as regular expressions.", diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueReplace.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueReplace.java index 5c2df1ce7..50fa8371f 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueReplace.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueReplace.java @@ -62,9 +62,13 @@ */ public class AttributeValueReplace extends AbstractValueProcessing { - public static final String PARAMETER_REPLACE_WHAT = "replace_what"; + /** @deprecated since 9.3; use {@link ParameterTypeRegexp#PARAMETER_REPLACE_WHAT} instead */ + @Deprecated + public static final String PARAMETER_REPLACE_WHAT = ParameterTypeRegexp.PARAMETER_REPLACE_WHAT; - public static final String PARAMETER_REPLACE_BY = "replace_by"; + /** @deprecated since 9.3; use {@link ParameterTypeRegexp#PARAMETER_REPLACE_BY} instead */ + @Deprecated + public static final String PARAMETER_REPLACE_BY = ParameterTypeRegexp.PARAMETER_REPLACE_BY; public AttributeValueReplace(OperatorDescription description) { super(description); @@ -72,7 +76,7 @@ public AttributeValueReplace(OperatorDescription description) { @Override public ExampleSetMetaData applyOnFilteredMetaData(ExampleSetMetaData emd) throws UndefinedParameterError { - String replaceWhat = getParameterAsString(PARAMETER_REPLACE_WHAT); + String replaceWhat = getParameterAsString(ParameterTypeRegexp.PARAMETER_REPLACE_WHAT); Pattern whatPattern; try { whatPattern = Pattern.compile(replaceWhat); @@ -81,8 +85,8 @@ public ExampleSetMetaData applyOnFilteredMetaData(ExampleSetMetaData emd) throws return emd; } String replaceBy = ""; - if (isParameterSet(PARAMETER_REPLACE_BY)) { - replaceBy = getParameterAsString(PARAMETER_REPLACE_BY); + if (isParameterSet(ParameterTypeRegexp.PARAMETER_REPLACE_BY)) { + replaceBy = getParameterAsString(ParameterTypeRegexp.PARAMETER_REPLACE_BY); } for (AttributeMetaData amd : emd.getAllAttributes()) { Set valueSet = new TreeSet<>(); @@ -106,7 +110,7 @@ public ExampleSetMetaData applyOnFilteredMetaData(ExampleSetMetaData emd) throws @Override public ExampleSet applyOnFiltered(ExampleSet exampleSet) throws OperatorException { - String replaceWhat = getParameterAsString(PARAMETER_REPLACE_WHAT); + String replaceWhat = getParameterAsString(ParameterTypeRegexp.PARAMETER_REPLACE_WHAT); Pattern whatPattern; try { whatPattern = Pattern.compile(replaceWhat); @@ -114,8 +118,8 @@ public ExampleSet applyOnFiltered(ExampleSet exampleSet) throws OperatorExceptio throw new UserError(this, 206, replaceWhat, e.getMessage()); } String replaceBy = ""; - if (isParameterSet(PARAMETER_REPLACE_BY)) { - replaceBy = getParameterAsString(PARAMETER_REPLACE_BY); + if (isParameterSet(ParameterTypeRegexp.PARAMETER_REPLACE_BY)) { + replaceBy = getParameterAsString(ParameterTypeRegexp.PARAMETER_REPLACE_BY); } LinkedHashMap attributeMap = new LinkedHashMap<>(); @@ -176,13 +180,15 @@ protected int[] getFilterValueTypes() { @Override public List getParameterTypes() { List types = super.getParameterTypes(); - ParameterType type = new ParameterTypeRegexp(PARAMETER_REPLACE_WHAT, "A regular expression specifying what should be replaced.", + ParameterTypeRegexp regexp = new ParameterTypeRegexp(ParameterTypeRegexp.PARAMETER_REPLACE_WHAT, "A regular expression specifying what should be replaced.", false, false); - type.setPrimary(true); - types.add(type); - types.add(new ParameterTypeString(PARAMETER_REPLACE_BY, + regexp.setPrimary(true); + types.add(regexp); + ParameterTypeString replacement = new ParameterTypeString(ParameterTypeRegexp.PARAMETER_REPLACE_BY, "The replacement for the region matched by the regular expression. Possibly including capturing groups.", - true, false)); + true, false); + regexp.setReplacementParameter(replacement); + types.add(replacement); return types; } diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/ChangeAttributeNamesReplace.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/ChangeAttributeNamesReplace.java index 8ae0074bd..a2e70ae4b 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/ChangeAttributeNamesReplace.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/ChangeAttributeNamesReplace.java @@ -56,9 +56,13 @@ */ public class ChangeAttributeNamesReplace extends AbstractDataProcessing { - public static final String PARAMETER_REPLACE_WHAT = "replace_what"; + /** @deprecated since 9.3; use {@link ParameterTypeRegexp#PARAMETER_REPLACE_WHAT} instead */ + @Deprecated + public static final String PARAMETER_REPLACE_WHAT = ParameterTypeRegexp.PARAMETER_REPLACE_WHAT; - public static final String PARAMETER_REPLACE_BY = "replace_by"; + /** @deprecated since 9.3; use {@link ParameterTypeRegexp#PARAMETER_REPLACE_BY} instead */ + @Deprecated + public static final String PARAMETER_REPLACE_BY = ParameterTypeRegexp.PARAMETER_REPLACE_BY; private final AttributeSubsetSelector attributeSelector = new AttributeSubsetSelector(this, getExampleSetInputPort()); @@ -71,9 +75,9 @@ protected MetaData modifyMetaData(ExampleSetMetaData exampleSetMetaData) { String replaceWhat = ""; try { ExampleSetMetaData subsetMetaData = attributeSelector.getMetaDataSubset(exampleSetMetaData, false); - replaceWhat = getParameterAsString(PARAMETER_REPLACE_WHAT); + replaceWhat = getParameterAsString(ParameterTypeRegexp.PARAMETER_REPLACE_WHAT); Pattern replaceWhatPattern = Pattern.compile(replaceWhat); - String replaceByString = isParameterSet(PARAMETER_REPLACE_BY) ? getParameterAsString(PARAMETER_REPLACE_BY) : ""; + String replaceByString = isParameterSet(ParameterTypeRegexp.PARAMETER_REPLACE_BY) ? getParameterAsString(ParameterTypeRegexp.PARAMETER_REPLACE_BY) : ""; for (AttributeMetaData attributeMetaData : subsetMetaData.getAllAttributes()) { String name = attributeMetaData.getName(); @@ -84,7 +88,7 @@ protected MetaData modifyMetaData(ExampleSetMetaData exampleSetMetaData) { } catch (UndefinedParameterError e) { } catch (IndexOutOfBoundsException e) { addError(new SimpleProcessSetupError(Severity.ERROR, getPortOwner(), "capturing_group_undefined", - PARAMETER_REPLACE_BY, PARAMETER_REPLACE_WHAT)); + ParameterTypeRegexp.PARAMETER_REPLACE_BY, ParameterTypeRegexp.PARAMETER_REPLACE_WHAT)); } catch (PatternSyntaxException e) { addError(new SimpleProcessSetupError(Severity.ERROR, getPortOwner(), "invalid_regex", replaceWhat)); } @@ -95,15 +99,15 @@ protected MetaData modifyMetaData(ExampleSetMetaData exampleSetMetaData) { @Override public ExampleSet apply(ExampleSet exampleSet) throws OperatorException { Set attributeSubset = attributeSelector.getAttributeSubset(exampleSet, false); - Pattern replaceWhatPattern = Pattern.compile(getParameterAsString(PARAMETER_REPLACE_WHAT)); - String replaceByString = isParameterSet(PARAMETER_REPLACE_BY) ? getParameterAsString(PARAMETER_REPLACE_BY) : ""; + Pattern replaceWhatPattern = Pattern.compile(getParameterAsString(ParameterTypeRegexp.PARAMETER_REPLACE_WHAT)); + String replaceByString = isParameterSet(ParameterTypeRegexp.PARAMETER_REPLACE_BY) ? getParameterAsString(ParameterTypeRegexp.PARAMETER_REPLACE_BY) : ""; try { for (Attribute attribute : attributeSubset) { attribute.setName(replaceWhatPattern.matcher(attribute.getName()).replaceAll(replaceByString)); } } catch (IndexOutOfBoundsException e) { - throw new UserError(this, 215, replaceByString, PARAMETER_REPLACE_WHAT); + throw new UserError(this, 215, replaceByString, ParameterTypeRegexp.PARAMETER_REPLACE_WHAT); } return exampleSet; @@ -114,16 +118,18 @@ public List getParameterTypes() { List types = super.getParameterTypes(); types.addAll(attributeSelector.getParameterTypes()); - ParameterType type = new ParameterTypeRegexp(PARAMETER_REPLACE_WHAT, + ParameterTypeRegexp regexp = new ParameterTypeRegexp(ParameterTypeRegexp.PARAMETER_REPLACE_WHAT, "A regular expression defining what should be replaced in the attribute names.", "\\W"); - type.setShowRange(false); - type.setExpert(false); - type.setPrimary(true); - types.add(type); + regexp.setShowRange(false); + regexp.setExpert(false); + regexp.setPrimary(true); + types.add(regexp); - types.add(new ParameterTypeString(PARAMETER_REPLACE_BY, + ParameterTypeString replacement = new ParameterTypeString(ParameterTypeRegexp.PARAMETER_REPLACE_BY, "This string is used as replacement for all parts of the matching attributes where the parameter '" - + PARAMETER_REPLACE_WHAT + "' matches.", true, false)); + + ParameterTypeRegexp.PARAMETER_REPLACE_WHAT + "' matches.", true, false); + regexp.setReplacementParameter(replacement); + types.add(replacement); return types; } diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/NominalToBinominal.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/NominalToBinominal.java index 553227d81..364c6421c 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/NominalToBinominal.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/NominalToBinominal.java @@ -19,7 +19,6 @@ package com.rapidminer.operator.preprocessing.filter; import java.util.Collection; -import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -71,30 +70,41 @@ public NominalToBinominal(OperatorDescription description) { @Override protected Collection modifyAttributeMetaData(ExampleSetMetaData emd, AttributeMetaData amd) { - boolean transformBinominal = getParameterAsBoolean(PARAMETER_TRANSFORM_BINOIMINAL); if (amd.isNominal()) { - Collection newAttributeMetaDataCollection = new LinkedList(); - if (!transformBinominal && amd.getValueSet().size() == 2) { + LinkedList newAttributeMetaDataCollection = new LinkedList<>(); + boolean transformBinomial = getParameterAsBoolean(PARAMETER_TRANSFORM_BINOIMINAL); + boolean useUnderscoreInName = getParameterAsBoolean(PARAMETER_USE_UNDERSCORE_IN_NAME); + if (!transformBinomial && amd.getValueSet().size() == 2) { amd.setType(Ontology.BINOMINAL); - return Collections.singletonList(amd); + amd.setValueSetRelation(SetRelation.EQUAL); + newAttributeMetaDataCollection.add(amd); + } else if (amd.getValueSetRelation() == SetRelation.UNKNOWN) { + String name = amd.getName(); + if (transformBinomial) { + // In this case we know that the original variable has been replaced. But we do not know the + // names and number of the new attributes. Therefore, we mark the variable name with ? to show + // that the meta data is incorrect. + name += (useUnderscoreInName ? "_" : " = ") + "?"; + AttributeMetaData newAttributeMetaData = newBinomialAttributeMetaData(name); + newAttributeMetaDataCollection.add(newAttributeMetaData); + } else { + // We assume the value set is of size 2 (binomial) because it is the most common case and + // we do not have any information about the attribute's value set. + // Whenever this assumption is wrong the meta data will be incorrect. + amd.setType(Ontology.BINOMINAL); + amd.setValueSetRelation(SetRelation.UNKNOWN); + newAttributeMetaDataCollection.add(amd); + } } else { - if (amd.getValueSetRelation() != SetRelation.UNKNOWN) { - boolean useUnderscoreInName = getParameterAsBoolean(PARAMETER_USE_UNDERSCORE_IN_NAME); - for (String value : amd.getValueSet()) { - String name = amd.getName() + (useUnderscoreInName ? "_" : " = ") + value; - AttributeMetaData newAttributeMetaData = new AttributeMetaData(name, Ontology.BINOMINAL); - Set values = new TreeSet(); - values.add("false"); - values.add("true"); - newAttributeMetaData.setValueSet(values, SetRelation.EQUAL); - newAttributeMetaDataCollection.add(newAttributeMetaData); - emd.mergeSetRelation(amd.getValueSetRelation()); - } + for (String value : amd.getValueSet()) { + String name = amd.getName() + (useUnderscoreInName ? "_" : " = ") + value; + AttributeMetaData newAttributeMetaData = newBinomialAttributeMetaData(name); + newAttributeMetaDataCollection.add(newAttributeMetaData); } - return newAttributeMetaDataCollection; } + return newAttributeMetaDataCollection; } else { - return null; // Collections.singleton(amd); + return null; } } @@ -148,4 +158,20 @@ public OperatorVersion[] getIncompatibleVersionChanges() { return (OperatorVersion[]) ArrayUtils.addAll(super.getIncompatibleVersionChanges(), new OperatorVersion[] { VERSION_MAY_WRITE_INTO_DATA }); } + + /** + * Helper method that creates binomial attribute meta data. + * + * @param name + * the attributes name + * @return new meta data + */ + private AttributeMetaData newBinomialAttributeMetaData(String name) { + AttributeMetaData newAttributeMetaData = new AttributeMetaData(name, Ontology.BINOMINAL); + Set values = new TreeSet<>(); + values.add("false"); + values.add("true"); + newAttributeMetaData.setValueSet(values, SetRelation.EQUAL); + return newAttributeMetaData; + } } diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/Real2Integer.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/Real2Integer.java index 966a68782..8a27ca622 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/Real2Integer.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/Real2Integer.java @@ -58,6 +58,12 @@ public class Real2Integer extends AbstractFilteredDataProcessing { */ private static final OperatorVersion VERSION_MAY_WRITE_INTO_DATA = new OperatorVersion(7, 1, 1); + /** + * Old version converts infinite real values to Long.MAX_VALUE or Long.MIN_VALUE. The new version preserves infinite + * values. + */ + private static final OperatorVersion VERSION_CAN_NOT_HANDLE_INFINITY = new OperatorVersion(9, 2, 1); + public Real2Integer(OperatorDescription description) { super(description); } @@ -71,14 +77,9 @@ public ExampleSetMetaData applyOnFilteredMetaData(ExampleSetMetaData emd) { && (!Ontology.ATTRIBUTE_VALUE_TYPE.isA(amd.getValueType(), Ontology.INTEGER))) { amd.setType(Ontology.INTEGER); } - if (round) { - amd.setValueRange( - new Range(Math.round(amd.getValueRange().getLower()), Math.round(amd.getValueRange().getUpper())), - SetRelation.EQUAL); - } else { - amd.setValueRange(new Range((long) amd.getValueRange().getLower(), (long) amd.getValueRange().getUpper()), - SetRelation.EQUAL); - } + double lower = realToInt(round, amd.getValueRange().getLower()); + double upper = realToInt(round, amd.getValueRange().getUpper()); + amd.setValueRange(new Range(lower, upper), SetRelation.EQUAL); } return emd; } @@ -100,6 +101,9 @@ public ExampleSet applyOnFiltered(ExampleSet exampleSet) throws OperatorExceptio double originalValue = example.getValue(attribute); if (Double.isNaN(originalValue)) { example.setValue(newAttribute, Double.NaN); + } else if (Double.isInfinite(originalValue) && + getCompatibilityLevel().isAbove(VERSION_CAN_NOT_HANDLE_INFINITY)) { + example.setValue(newAttribute, originalValue); } else { long newValue = round ? Math.round(originalValue) : (long) originalValue; example.setValue(newAttribute, newValue); @@ -147,6 +151,24 @@ public ResourceConsumptionEstimator getResourceConsumptionEstimator() { @Override public OperatorVersion[] getIncompatibleVersionChanges() { return (OperatorVersion[]) ArrayUtils.addAll(super.getIncompatibleVersionChanges(), - new OperatorVersion[] { VERSION_MAY_WRITE_INTO_DATA }); + new OperatorVersion[]{VERSION_MAY_WRITE_INTO_DATA, VERSION_CAN_NOT_HANDLE_INFINITY }); + } + + /** + * Helper method that either transforms the real to an int by rounding or by ignoring the fractional part. + */ + private double realToInt(boolean round, double real) { + double result; + if (!Double.isFinite(real) && getCompatibilityLevel().isAbove(VERSION_CAN_NOT_HANDLE_INFINITY)) { + // preserves pos/neg infinity and NaN + result = real; + } else { + if (round) { + result = Math.round(real); + } else { + result = (long) real; + } + } + return result; } } diff --git a/src/main/java/com/rapidminer/operator/preprocessing/join/ExampleSetMerge.java b/src/main/java/com/rapidminer/operator/preprocessing/join/ExampleSetMerge.java index 8ed3d4fb9..120fff1c4 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/join/ExampleSetMerge.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/join/ExampleSetMerge.java @@ -139,7 +139,7 @@ public void transformMD() { // now unify all single attributes meta data if (emds.size() > 0) { - ExampleSetMetaData resultEMD = emds.get(0); + ExampleSetMetaData resultEMD = emds.get(0).clone(); for (int i = 1; i < emds.size(); i++) { ExampleSetMetaData mergerEMD = emds.get(i); resultEMD.getNumberOfExamples().add(mergerEMD.getNumberOfExamples()); diff --git a/src/main/java/com/rapidminer/operator/visualization/ProcessLogOperator.java b/src/main/java/com/rapidminer/operator/visualization/ProcessLogOperator.java index 53d73fc15..cbd9491f8 100644 --- a/src/main/java/com/rapidminer/operator/visualization/ProcessLogOperator.java +++ b/src/main/java/com/rapidminer/operator/visualization/ProcessLogOperator.java @@ -27,6 +27,7 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.logging.Level; import com.rapidminer.datatable.DataTable; import com.rapidminer.datatable.DataTableRow; @@ -37,11 +38,11 @@ import com.rapidminer.operator.OperatorException; import com.rapidminer.operator.ProcessSetupError.Severity; import com.rapidminer.operator.SimpleProcessSetupError; +import com.rapidminer.operator.UndefinedParameterSetupError; import com.rapidminer.operator.UserError; import com.rapidminer.operator.Value; import com.rapidminer.operator.ports.DummyPortPairExtender; import com.rapidminer.operator.ports.PortPairExtender; -import com.rapidminer.operator.ports.metadata.MDTransformationRule; import com.rapidminer.operator.ports.quickfix.ParameterSettingQuickFix; import com.rapidminer.parameter.ParameterType; import com.rapidminer.parameter.ParameterTypeBoolean; @@ -118,64 +119,111 @@ public ProcessLogOperator(OperatorDescription description) { getTransformer().addRule(dummyPorts.makePassThroughRule()); // check if the user entered duplicate column names - getTransformer().addRule(new MDTransformationRule() { - - @Override - public void transformMD() { - try { - getColumnNames(); - } catch (UserError e) { - addError(new SimpleProcessSetupError(Severity.INFORMATION, ProcessLogOperator.this.getPortOwner(), - Collections.singletonList(new ParameterSettingQuickFix(ProcessLogOperator.this, PARAMETER_LOG)), - "duplicate_log_column")); - } + getTransformer().addRule(() -> { + try { + getColumnNames(); + } catch (UserError e) { + addError(new SimpleProcessSetupError(Severity.INFORMATION, ProcessLogOperator.this.getPortOwner(), + Collections.singletonList(new ParameterSettingQuickFix(ProcessLogOperator.this, PARAMETER_LOG)), + "duplicate_log_column")); } }); } + @Override + protected void performAdditionalChecks() { + File file = null; + try { + file = getParameterAsFile(PARAMETER_FILENAME); + } catch (UserError e) { + // tries to determine a file for output writing + // if no file was specified -> do not write results in file + } + if (file == null) { + if (getParameterAsBoolean(PARAMETER_PERSISTENT)) { + addError(new UndefinedParameterSetupError(this, PARAMETER_FILENAME)); + } + return; + } + boolean existedBefore = file.exists(); + File topExisting = null; + List fileNameQuickFix = Collections.singletonList(new ParameterSettingQuickFix(this, PARAMETER_FILENAME)); + if (!existedBefore) { + topExisting = file; + while (topExisting != null && !topExisting.exists()) { + topExisting = topExisting.getParentFile(); + } + if (topExisting == null) { + addError(new SimpleProcessSetupError(Severity.ERROR, getPortOwner(), fileNameQuickFix, + "io.invalid_filepath", file.getAbsolutePath())); + return; + } + try { + file = getParameterAsFile(PARAMETER_FILENAME, true); + } catch (UserError e) { + // file cannot be created -> error and stop + addError(new SimpleProcessSetupError(Severity.ERROR, getPortOwner(), fileNameQuickFix, + "io.dir_creation_fail", file.getAbsolutePath(), e.getMessage())); + return; + } + } + try (FileWriter fw = new FileWriter(file, existedBefore)) { + // test if writable + } catch (IOException e) { + addError(new SimpleProcessSetupError(Severity.ERROR, getPortOwner(), fileNameQuickFix, + "io.writing_fail", file.getAbsolutePath(), e.getLocalizedMessage())); + } + if (!existedBefore) { + // clean up created directories + do { + file = file.getParentFile(); + } while (!topExisting.equals(file) && file.delete()); + } + } + private double fetchValue(OperatorValueSelection selection, int column) throws UndefinedParameterError { Operator operator = lookupOperator(selection.getOperator()); - if (operator != null) { - if (selection.isValue()) { - Value value = operator.getValue(selection.getValueName()); - if (value == null) { - getLogger().warning("No such value in '" + selection + "'"); - return Double.NaN; - } - if (value.isNominal()) { - Object actualValue = value.getValue(); - if (actualValue != null) { - String valueString = value.getValue().toString(); - SimpleDataTable table = (SimpleDataTable) getProcess().getDataTable(getName()); - return table.mapString(column, valueString); - } else { - return Double.NaN; - } + if (operator == null) { + logWarning("Unknown operator '" + selection.getOperator() + "' in '" + selection + "'"); + return Double.NaN; + } + if (selection.isValue()) { + Value value = operator.getValue(selection.getValueName()); + if (value == null) { + getLogger().warning("No such value in '" + selection + "'"); + return Double.NaN; + } + if (value.isNominal()) { + Object actualValue = value.getValue(); + if (actualValue != null) { + String valueString = value.getValue().toString(); + SimpleDataTable table = (SimpleDataTable) getProcess().getDataTable(getName()); + return table.mapString(column, valueString); } else { - return ((Double) value.getValue()).doubleValue(); + return Double.NaN; } + } else { + return (Double) value.getValue(); + } + } else { + ParameterType parameterType = operator.getParameterType(selection.getParameterName()); + if (parameterType == null) { + logWarning("No such parameter in '" + selection + "'"); + return Double.NaN; } else { - ParameterType parameterType = operator.getParameterType(selection.getParameterName()); - if (parameterType == null) { - logWarning("No such parameter in '" + selection + "'"); - return Double.NaN; - } else { - if (parameterType.isNumerical()) { // numerical - try { - return Double.parseDouble(operator.getParameter(selection.getParameterName()).toString()); - } catch (NumberFormatException e) { - logWarning("Cannot parse parameter value of '" + selection + "'"); - } - } else { // nominal - String value = parameterType.toString(operator.getParameter(selection.getParameterName())); - SimpleDataTable table = (SimpleDataTable) getProcess().getDataTable(getName()); - return table.mapString(column, value); + if (parameterType.isNumerical()) { // numerical + try { + return operator.getParameterAsDouble(selection.getParameterName()); + } catch (UndefinedParameterError e) { + logWarning("Cannot parse parameter value of '" + selection + "'"); } + } else { // nominal + String value = parameterType.toString(operator.getParameter(selection.getParameterName())); + SimpleDataTable table = (SimpleDataTable) getProcess().getDataTable(getName()); + return table.mapString(column, value); } } - } else { - logWarning("Unknown operator '" + selection.getOperator() + "' in '" + selection + "'"); } return Double.NaN; } @@ -201,8 +249,20 @@ public void doWork() throws OperatorException { } DataTableRow row = fetchAllValues(); - if (getParameterAsInt(PARAMETER_SORTING_TYPE) == SORTING_TYPE_NONE && getParameterAsBoolean(PARAMETER_PERSISTENT)) { - writeOnline(row); + if (getParameterAsInt(PARAMETER_SORTING_TYPE) == SORTING_TYPE_NONE && getParameterAsBoolean(PARAMETER_PERSISTENT) && row.getNumberOfValues() > 0) { + File logFile = null; + try { + logFile = getParameterAsFile(PARAMETER_FILENAME); + } catch (UserError e) { + } + if (logFile == null) { + // only log once, e.g. in a loop + if (getApplyCount() < 2) { + getLogger().log(Level.WARNING, "com.rapidminer.ProcessLogOperator.unspecified_logfile", getName()); + } + } else { + writeOnline(row); + } } dummyPorts.passDataThrough(); @@ -238,64 +298,70 @@ private void writeOnline(DataTableRow row) throws UserError { private DataTableRow fetchAllValues() throws UndefinedParameterError { Collection valueDescriptions = getValueDescriptions(); double[] row = new double[valueDescriptions.size()]; + if (row.length == 0) { + if (getApplyCount() < 2) { + getLogger().log(Level.WARNING, "com.rapidminer.ProcessLogOperator.empty_loglist", getName()); + } + return new SimpleDataTableRow(row); + } int i = 0; for (OperatorValueSelection selection : valueDescriptions) { row[i] = fetchValue(selection, i); i++; } - DataTableRow dataRow = new SimpleDataTableRow(row, null); + DataTableRow dataRow = new SimpleDataTableRow(row); SimpleDataTable dataTable = (SimpleDataTable) getProcess().getDataTable(getName()); int sortingType = getParameterAsInt(PARAMETER_SORTING_TYPE); if (sortingType == SORTING_TYPE_NONE || dataTable.getNumberOfRows() < getParameterAsInt(PARAMETER_SORTING_K)) { dataTable.add(dataRow); - } else { - // sorting - String sortingDimension = getParameterAsString(PARAMETER_SORTING_DIMENSION); - int sortingDimensionIndex = dataTable.getColumnIndex(sortingDimension); - - if (dataTable.isNominal(sortingDimensionIndex)) { - String currentWorst = null; - int currentWorstIndex = -1; - for (int r = 0; r < dataTable.getNumberOfRows(); r++) { - double currentValue = dataTable.getRow(r).getValue(sortingDimensionIndex); - String currentNominalValue = dataTable.mapIndex(sortingDimensionIndex, (int) currentValue); - if (currentWorst == null || sortingType == SORTING_TYPE_TOP_K - && currentNominalValue.compareTo(currentWorst) < 0 || sortingType == SORTING_TYPE_BOTTOM_K - && currentNominalValue.compareTo(currentWorst) > 0) { - currentWorst = currentNominalValue; - currentWorstIndex = r; - } + return dataRow; + } + // sorting + String sortingDimension = getParameterAsString(PARAMETER_SORTING_DIMENSION); + int sortingDimensionIndex = dataTable.getColumnIndex(sortingDimension); + + if (dataTable.isNominal(sortingDimensionIndex)) { + String currentWorst = null; + int currentWorstIndex = -1; + for (int r = 0; r < dataTable.getNumberOfRows(); r++) { + double currentValue = dataTable.getRow(r).getValue(sortingDimensionIndex); + String currentNominalValue = dataTable.mapIndex(sortingDimensionIndex, (int) currentValue); + if (currentWorst == null || sortingType == SORTING_TYPE_TOP_K + && currentNominalValue.compareTo(currentWorst) < 0 || sortingType == SORTING_TYPE_BOTTOM_K + && currentNominalValue.compareTo(currentWorst) > 0) { + currentWorst = currentNominalValue; + currentWorstIndex = r; } + } - double candidateValue = dataRow.getValue(sortingDimensionIndex); - String candidateNominalValue = dataTable.mapIndex(sortingDimensionIndex, (int) candidateValue); - if (currentWorstIndex >= 0 && sortingType == SORTING_TYPE_TOP_K - && candidateNominalValue.compareTo(currentWorst) > 0 || sortingType == SORTING_TYPE_BOTTOM_K - && candidateNominalValue.compareTo(currentWorst) < 0) { - dataTable.remove(dataTable.getRow(currentWorstIndex)); - dataTable.add(dataRow); - dataTable.cleanMappingTables(); - } - } else { - double currentWorst = Double.NaN; - int currentWorstIndex = -1; - for (int r = 0; r < dataTable.getNumberOfRows(); r++) { - double currentValue = dataTable.getRow(r).getValue(sortingDimensionIndex); - if (Double.isNaN(currentWorst) || sortingType == SORTING_TYPE_TOP_K && currentValue < currentWorst - || sortingType == SORTING_TYPE_BOTTOM_K && currentValue > currentWorst) { - currentWorst = currentValue; - currentWorstIndex = r; - } + double candidateValue = dataRow.getValue(sortingDimensionIndex); + String candidateNominalValue = dataTable.mapIndex(sortingDimensionIndex, (int) candidateValue); + if (currentWorstIndex >= 0 && sortingType == SORTING_TYPE_TOP_K + && candidateNominalValue.compareTo(currentWorst) > 0 || sortingType == SORTING_TYPE_BOTTOM_K + && candidateNominalValue.compareTo(currentWorst) < 0) { + dataTable.remove(dataTable.getRow(currentWorstIndex)); + dataTable.add(dataRow); + dataTable.cleanMappingTables(); + } + } else { + double currentWorst = Double.NaN; + int currentWorstIndex = -1; + for (int r = 0; r < dataTable.getNumberOfRows(); r++) { + double currentValue = dataTable.getRow(r).getValue(sortingDimensionIndex); + if (Double.isNaN(currentWorst) || sortingType == SORTING_TYPE_TOP_K && currentValue < currentWorst + || sortingType == SORTING_TYPE_BOTTOM_K && currentValue > currentWorst) { + currentWorst = currentValue; + currentWorstIndex = r; } + } - double candidateValue = dataRow.getValue(sortingDimensionIndex); - if (currentWorstIndex >= 0 && sortingType == SORTING_TYPE_TOP_K && candidateValue > currentWorst - || sortingType == SORTING_TYPE_BOTTOM_K && candidateValue < currentWorst) { - dataTable.remove(dataTable.getRow(currentWorstIndex)); - dataTable.add(dataRow); - dataTable.cleanMappingTables(); - } + double candidateValue = dataRow.getValue(sortingDimensionIndex); + if (currentWorstIndex >= 0 && sortingType == SORTING_TYPE_TOP_K && candidateValue > currentWorst + || sortingType == SORTING_TYPE_BOTTOM_K && candidateValue < currentWorst) { + dataTable.remove(dataTable.getRow(currentWorstIndex)); + dataTable.add(dataRow); + dataTable.cleanMappingTables(); } } return dataRow; @@ -305,24 +371,26 @@ private DataTableRow fetchAllValues() throws UndefinedParameterError { public void processFinished() throws OperatorException { super.processFinished(); - if (!getParameterAsBoolean(PARAMETER_PERSISTENT)) { - DataTable table = getProcess().getDataTable(getName()); - if (table != null) { - File file = null; - try { - file = getParameterAsFile(PARAMETER_FILENAME, true); - } catch (UndefinedParameterError e) { - // tries to determine a file for output writing - // if no file was specified --> do not write results in file - } - if (file != null) { - log("Writing data to '" + file.getName() + "'"); - try (FileWriter fw = new FileWriter(file); PrintWriter out = new PrintWriter(fw)) { - table.write(out); - } catch (IOException e) { - throw new UserError(this, 303, file.getName(), e.getMessage()); - } - } + if (getParameterAsBoolean(PARAMETER_PERSISTENT)) { + return; + } + DataTable table = getProcess().getDataTable(getName()); + if (table == null) { + return; + } + File file = null; + try { + file = getParameterAsFile(PARAMETER_FILENAME, true); + } catch (UndefinedParameterError e) { + // tries to determine a file for output writing + // if no file was specified --> do not write results in file + } + if (file != null) { + log("Writing data to '" + file.getName() + "'"); + try (FileWriter fw = new FileWriter(file); PrintWriter out = new PrintWriter(fw)) { + table.write(out); + } catch (IOException e) { + throw new UserError(this, 303, file.getName(), e.getMessage()); } } } @@ -339,8 +407,8 @@ public List getParameterTypes() { PARAMETER_LOG, "List of key value pairs where the key is the column name and the value specifies the process value to log.", new ParameterTypeString(PARAMETER_COLUMN_NAME, "The name of the column in the process log."), - new ParameterTypeValue(PARAMETER_COLUMN_VALUE, "operator.OPERATORNAME.[value|parameter].VALUE_NAME")); - type.setExpert(false); + new ParameterTypeValue(PARAMETER_COLUMN_VALUE, "operator.OPERATORNAME.[value|parameter].VALUE_NAME"), false); + type.setOptional(false); type.setPrimary(true); types.add(type); @@ -379,7 +447,7 @@ public List getParameterTypes() { */ private String[] getColumnNames() throws UserError { List parameters = getParameterList(PARAMETER_LOG); - String columnNames[] = new String[parameters.size()]; + String[] columnNames = new String[parameters.size()]; Iterator i = parameters.iterator(); int counter = 0; while (i.hasNext()) { diff --git a/src/main/java/com/rapidminer/operator/visualization/ROCBasedComparisonOperator.java b/src/main/java/com/rapidminer/operator/visualization/ROCBasedComparisonOperator.java index d1ea27278..234db8f29 100644 --- a/src/main/java/com/rapidminer/operator/visualization/ROCBasedComparisonOperator.java +++ b/src/main/java/com/rapidminer/operator/visualization/ROCBasedComparisonOperator.java @@ -135,6 +135,11 @@ public void doWork() throws OperatorException { getParameterAsInt(RandomGenerator.PARAMETER_LOCAL_RANDOM_SEED), getCompatibilityLevel().isAtMost(SplittedExampleSet.VERSION_SAMPLING_CHANGED)); + // It is important to remove any predicted labels that are possibly part of the input example set + // before we start our own predictions. Otherwise these predicted labels might be removed + // (alongside our own) from the underlying example table, possibly leading to side effects + PredictionModel.removePredictedLabel(eSet, false, false); + // apply subprocess to generate all models eSet.selectSingleSubset(0); trainingSetExtender.deliverToAll(eSet, false); @@ -166,7 +171,10 @@ public void doWork() throws OperatorException { getParameterAsInt(RandomGenerator.PARAMETER_LOCAL_RANDOM_SEED), getCompatibilityLevel().isAtMost(SplittedExampleSet.VERSION_SAMPLING_CHANGED)); - PredictionModel.removePredictedLabel(eSet); + // It is important to remove any predicted labels that are possibly part of the input example set + // before we start our own predictions. Otherwise the original predicted labels might be removed + // (alongside our own) from the underlying example table, possibly leading to side effects + PredictionModel.removePredictedLabel(eSet, false, false); for (int iteration = 0; iteration < numberOfFolds; iteration++) { eSet.selectAllSubsetsBut(iteration); diff --git a/src/main/java/com/rapidminer/operator/visualization/dependencies/NumericalMatrix.java b/src/main/java/com/rapidminer/operator/visualization/dependencies/NumericalMatrix.java index 39fb9e4e9..b9590a5c7 100644 --- a/src/main/java/com/rapidminer/operator/visualization/dependencies/NumericalMatrix.java +++ b/src/main/java/com/rapidminer/operator/visualization/dependencies/NumericalMatrix.java @@ -54,6 +54,9 @@ public class NumericalMatrix extends ResultObjectAdapter { private String name; + private double theoreticalMin = Double.NaN; + private double theoreticalMax = Double.NaN; + private boolean symmetrical = false; private String firstAttributeName = "First Attribute"; @@ -167,6 +170,49 @@ public void setUseless(boolean useless) { isUseless = useless; } + /** + * Sets the theoretical min value that can appear in this matrix (e.g. -1 for correlation matrix). + * + * @param min + * the min value or {@link Double#NaN} + * @since 9.2.1 + */ + public void setTheoreticalMin(double min) { + this.theoreticalMin = min; + } + + /** + * Gets the theoretical min value that can appear in this matrix (e.g. -1 for correlation matrix). + * + * @return the min value or {@link Double#NaN} if not set + * @since 9.2.1 + */ + public double getTheoreticalMin() { + return theoreticalMin; + } + + /** + * Sets the theoretical max value that can appear in this matrix (e.g. 1 for correlation matrix). + * + * @param max + * the max value or {@link Double#NaN} + * @since 9.2.1 + */ + public void setTheoreticalMax(double max) { + this.theoreticalMax = max; + } + + /** + * Gets the theoretical max value that can appear in this matrix (e.g. 1 for correlation matrix). + * + * @return the max value or {@link Double#NaN} if not set + * @since 9.2.1 + */ + public double getTheoreticalMax() { + return theoreticalMax; + } + + public DataTable createMatrixDataTable() { return new DataTableSymmetricalMatrixAdapter(this, this.name, this.columnNames); } diff --git a/src/main/java/com/rapidminer/parameter/ParameterType.java b/src/main/java/com/rapidminer/parameter/ParameterType.java index 2edb5e29a..665151752 100644 --- a/src/main/java/com/rapidminer/parameter/ParameterType.java +++ b/src/main/java/com/rapidminer/parameter/ParameterType.java @@ -352,13 +352,36 @@ public boolean isPrimary() { } /** - * This method gives a hook for the parameter type to react on a renaming of an operator. It - * must return the correctly modified String value. The default implementation does nothing. + * This method gives a hook for the parameter type to react to the renaming of an operator. It + * must return the correctly modified parameter value as string. + * + * @return the unmodified parameterValue by default */ public String notifyOperatorRenaming(String oldOperatorName, String newOperatorName, String parameterValue) { return parameterValue; } + /** + * This method gives a hook for the parameter type to react to the replacing of an operator. It + * must return the correctly modified parameter value as string. + * + * @param oldName + * the name of the old operator; must not be {@code null} + * @param oldOp + * the old operator; can be {@code null} + * @param newName + * the name of the new operator; must not be {@code null} + * @param newOp + * the new operator; must not be {@code null} + * @param parameterValue + * the original parameter value + * @return the same as {@link #notifyOperatorRenaming(String, String, String) notifyOperatorRenaming} by default + * @since 9.3 + */ + public String notifyOperatorReplacing(String oldName, Operator oldOp, String newName, Operator newOp, String parameterValue) { + return notifyOperatorRenaming(oldName, newName, parameterValue); + } + /** Returns a string representation of this value. */ public String toString(Object value) { if (value == null) { diff --git a/src/main/java/com/rapidminer/parameter/ParameterTypeAttribute.java b/src/main/java/com/rapidminer/parameter/ParameterTypeAttribute.java index ec4c91f16..0f8252b4c 100644 --- a/src/main/java/com/rapidminer/parameter/ParameterTypeAttribute.java +++ b/src/main/java/com/rapidminer/parameter/ParameterTypeAttribute.java @@ -18,6 +18,8 @@ */ package com.rapidminer.parameter; +import java.util.ArrayList; +import java.util.List; import java.util.Vector; import java.util.stream.Collectors; @@ -151,42 +153,35 @@ public Vector getAttributeNames() { * @return the vector of pairs between the attribute names and their value type * @since 9.2.0 */ - public Vector> getAttributeNamesAndTypes(boolean sortAttributes) { - Vector> names = new Vector<>(); - Vector> regularNames = new Vector<>(); - + public List> getAttributeNamesAndTypes(boolean sortAttributes) { + List> names = new ArrayList<>(); + List> regularNames = new ArrayList<>(); MetaData metaData = getMetaData(); - if (metaData != null) { - if (metaData instanceof ExampleSetMetaData) { - ExampleSetMetaData emd = (ExampleSetMetaData) metaData; - for (AttributeMetaData amd : emd.getAllAttributes()) { - if (!isFilteredOut(amd) && isOfAllowedType(amd.getValueType())) { - if (amd.isSpecial()) { - names.add(new Pair<>(amd.getName(), amd.getValueType())); - } else { - regularNames.add(new Pair<>(amd.getName(), amd.getValueType())); - } - } - - } - } else if (metaData instanceof ModelMetaData) { - ModelMetaData mmd = (ModelMetaData) metaData; - ExampleSetMetaData emd = mmd.getTrainingSetMetaData(); - if (emd != null) { - for (AttributeMetaData amd : emd.getAllAttributes()) { - if (!isFilteredOut(amd) && isOfAllowedType(amd.getValueType())) { - if (amd.isSpecial()) { - names.add(new Pair<>(amd.getName(), amd.getValueType())); - } else { - regularNames.add(new Pair<>(amd.getName(), amd.getValueType())); - } - } - } + if (metaData == null) { + return names; + } + ExampleSetMetaData emd = null; + if (metaData instanceof ExampleSetMetaData) { + emd = (ExampleSetMetaData) metaData; + } else if (metaData instanceof ModelMetaData) { + ModelMetaData mmd = (ModelMetaData) metaData; + emd = mmd.getTrainingSetMetaData(); + } + if (emd == null) { + return names; + } + for (AttributeMetaData amd : emd.getAllAttributes()) { + if (!isFilteredOut(amd) && isOfAllowedType(amd.getValueType())) { + Pair nameAndType = new Pair<>(amd.getName(), amd.getValueType()); + if (amd.isSpecial()) { + names.add(nameAndType); + } else { + regularNames.add(nameAndType); } } } - if (sortAttributes) { + if (sortAttributes && (!names.isEmpty() || !regularNames.isEmpty())) { AlphanumComparator alphanumComparator = new AlphanumComparator(AlphanumComparator.AlphanumCaseSensitivity.INSENSITIVE); names.sort((o1, o2) -> alphanumComparator.compare(o1.getFirst(), o2.getFirst())); regularNames.sort((o1, o2) -> alphanumComparator.compare(o1.getFirst(), o2.getFirst())); @@ -225,7 +220,7 @@ public Object getDefaultValue() { */ protected boolean isFilteredOut(AttributeMetaData amd) { return false; - }; + } // public InputPort getInputPort() { // return inPort; diff --git a/src/main/java/com/rapidminer/parameter/ParameterTypeConnectionLocation.java b/src/main/java/com/rapidminer/parameter/ParameterTypeConnectionLocation.java new file mode 100644 index 000000000..775bb6d3b --- /dev/null +++ b/src/main/java/com/rapidminer/parameter/ParameterTypeConnectionLocation.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.parameter; + +/** + * Parameter type for selecting a {@link com.rapidminer.connection.ConnectionInformation} from the repository. + * Can narrow down the type of the connection if necessary + * + * @author Jan Czogalla + * @since 9.3 + */ +public class ParameterTypeConnectionLocation extends ParameterTypeRepositoryLocation { + + private String conType; + + /** + * Minimal constructor, accepting the location of a + * {@link com.rapidminer.connection.ConnectionInformation ConnectionInformation} of any type. + */ + public ParameterTypeConnectionLocation(String key, String description) { + this(key, description, null); + } + + /** Constructor with restricting the allowed type of the connection. */ + public ParameterTypeConnectionLocation(String key, String description, String conType) { + super(key, description, true, false, true, true); + setExpert(false); + this.conType = conType; + } + + /** Get the allowed type of connections. Returns {@code null} if any connection is allowed. */ + public String getConnectionType() { + return conType; + } +} diff --git a/src/main/java/com/rapidminer/parameter/ParameterTypeEnumeration.java b/src/main/java/com/rapidminer/parameter/ParameterTypeEnumeration.java index 1e722dd2a..f498932e6 100644 --- a/src/main/java/com/rapidminer/parameter/ParameterTypeEnumeration.java +++ b/src/main/java/com/rapidminer/parameter/ParameterTypeEnumeration.java @@ -19,7 +19,11 @@ package com.rapidminer.parameter; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.stream.Collectors; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -77,16 +81,14 @@ public ParameterTypeEnumeration(String key, String description, ParameterType pa public Element getXML(String key, String value, boolean hideDefault, Document doc) { Element element = doc.createElement("enumeration"); element.setAttribute("key", key); - String[] list = null; + String[] list; if (value != null) { list = transformString2Enumeration(value); } else { list = transformString2Enumeration(getDefaultValueAsString()); } - if (list != null) { - for (String string : list) { - element.appendChild(type.getXML(type.getKey(), string, false, doc)); - } + for (String string : list) { + element.appendChild(type.getXML(type.getKey(), string, false, doc)); } return element; } @@ -129,63 +131,81 @@ public ParameterType getValueType() { return type; } + /** @return the changed value after all entries were notified */ @Override public String notifyOperatorRenaming(String oldOperatorName, String newOperatorName, String parameterValue) { - String[] enumeratedValues = transformString2Enumeration(parameterValue); - for (int i = 0; i < enumeratedValues.length; i++) { - enumeratedValues[i] = type.notifyOperatorRenaming(oldOperatorName, newOperatorName, enumeratedValues[i]); - } - return transformEnumeration2String(Arrays.asList(enumeratedValues)); + return notifyOperatorRenamingReplacing((t, v) -> t.notifyOperatorRenaming(oldOperatorName, newOperatorName, v), parameterValue); + } + /** @return the changed value after all entries were notified */ + @Override + public String notifyOperatorReplacing(String oldName, Operator oldOp, String newName, Operator newOp, String parameterValue) { + return notifyOperatorRenamingReplacing((t, v) -> t.notifyOperatorReplacing(oldName, oldOp, newName, newOp, v), parameterValue); } + /** @since 9.3 */ + private String notifyOperatorRenamingReplacing(BiFunction replacer, String parameterValue) { + return transformEnumeration2String(Arrays.stream(transformString2Enumeration(parameterValue)) + .map(v -> replacer.apply(type, v)) + .collect(Collectors.toList())); + } + + /** + * Transforms the given list into an enumeration parameter string. Will escape all occurrences of + * {@link #SEPERATOR_CHAR the separator char} and join them with that same separator. + * + * @param list + * the list of individual parameter values + * @return the enumeration parameter string + */ public static String transformEnumeration2String(List list) { - StringBuilder builder = new StringBuilder(); - boolean isFirst = true; - for (String string : list) { - if (!isFirst) { - builder.append(SEPERATOR_CHAR); - } - if (string != null) { - builder.append(Tools.escape(string, ESCAPE_CHAR, SPECIAL_CHARACTERS)); - } - isFirst = false; - } - return builder.toString(); + return list.stream().filter(Objects::nonNull).map(string -> Tools.escape(string, ESCAPE_CHAR, SPECIAL_CHARACTERS)) + .collect(Collectors.joining(Character.toString(SEPERATOR_CHAR))); } - public static String[] transformString2Enumeration(String parameterValue) { + /** + * Transforms a parameter value into a list of strings representing each entry. + * Inverse method to {@link #transformEnumeration2String(List)}. + * @since 9.3 + */ + public static List transformString2List(String parameterValue) { if (parameterValue == null || "".equals(parameterValue)) { - return new String[0]; + return Collections.emptyList(); } - List split = Tools.unescape(parameterValue, ESCAPE_CHAR, SPECIAL_CHARACTERS, SEPERATOR_CHAR); - return split.toArray(new String[split.size()]); + return Tools.unescape(parameterValue, ESCAPE_CHAR, SPECIAL_CHARACTERS, SEPERATOR_CHAR); + } + + /** Same as {@link #transformString2List(String)}, but returns an array representation */ + public static String[] transformString2Enumeration(String parameterValue) { + return transformString2List(parameterValue).toArray(new String[0]); } @Override public String substituteMacros(String parameterValue, MacroHandler mh) throws UndefinedParameterError { - if (parameterValue.indexOf("%{") == -1) { + if (!parameterValue.contains("%{")) { return parameterValue; } - String[] list = transformString2Enumeration(parameterValue); - String[] result = new String[list.length]; - for (int i = 0; i < list.length; i++) { - result[i] = getValueType().substituteMacros(list[i], mh); + List list = transformString2List(parameterValue); + ParameterType valueType = getValueType(); + for (int i = 0; i < list.size(); i++) { + String value = list.get(i); + list.set(i, valueType.substituteMacros(value, mh)); } - return transformEnumeration2String(Arrays.asList(result)); + return transformEnumeration2String(list); } @Override public String substitutePredefinedMacros(String parameterValue, Operator operator) throws UndefinedParameterError { - if (parameterValue.indexOf("%{") == -1) { + if (!parameterValue.contains("%{")) { return parameterValue; } - String[] list = transformString2Enumeration(parameterValue); - String[] result = new String[list.length]; - for (int i = 0; i < list.length; i++) { - result[i] = getValueType().substitutePredefinedMacros(list[i], operator); + List list = transformString2List(parameterValue); + ParameterType valueType = getValueType(); + for (int i = 0; i < list.size(); i++) { + String value = list.get(i); + list.set(i, valueType.substitutePredefinedMacros(value, operator)); } - return transformEnumeration2String(Arrays.asList(result)); + return transformEnumeration2String(list); } /** @@ -195,7 +215,7 @@ public String substitutePredefinedMacros(String parameterValue, Operator operato */ @Override public boolean isSensitive() { - return getValueType() != null ? getValueType().isSensitive() : false; + return getValueType() != null && getValueType().isSensitive(); } @Override diff --git a/src/main/java/com/rapidminer/parameter/ParameterTypeExpression.java b/src/main/java/com/rapidminer/parameter/ParameterTypeExpression.java index 71e6adaa0..54334e31f 100644 --- a/src/main/java/com/rapidminer/parameter/ParameterTypeExpression.java +++ b/src/main/java/com/rapidminer/parameter/ParameterTypeExpression.java @@ -20,6 +20,8 @@ import java.util.concurrent.Callable; import java.util.logging.Level; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.w3c.dom.Element; @@ -31,6 +33,7 @@ import com.rapidminer.tools.LogService; import com.rapidminer.tools.XMLException; import com.rapidminer.tools.expression.ExpressionParserBuilder; +import com.rapidminer.tools.expression.internal.function.process.ParameterValue; /** @@ -47,6 +50,10 @@ public class ParameterTypeExpression extends ParameterTypeString { private static final long serialVersionUID = -1938925853519339382L; private static final String ATTRIBUTE_INPUT_PORT = "port-name"; + /** @since 9.3 */ + private static final String PARAMETER_VALUE_FUNCTION_NAME = new ParameterValue(null).getFunctionName(); + /** @since 9.3 */ + private static final String RENAMING_PATTERN = PARAMETER_VALUE_FUNCTION_NAME + " *\\( *\"(%s)\" *,.*?\\)"; private transient InputPort inPort; @@ -91,8 +98,7 @@ public ParameterTypeExpression(Element element) throws XMLException { * @param key * @param description * - * @deprecated use {@link #ParameterTypeExpression(String, String, OperatorVersionCallable)} - * instead + * @deprecated use {@link #ParameterTypeExpression(String, String, OperatorVersionCallable)} instead */ @Deprecated public ParameterTypeExpression(final String key, String description) { @@ -104,12 +110,12 @@ public ParameterTypeExpression(final String key, String description) { * associated {@link InputPort} to verify the expressions. * * @param key - * the parameter key + * the parameter key * @param description - * the parameter description + * the parameter description * @param operatorVersion - * a functional which allows to query the current operator version. Must not be - * {@code null} and must not return null + * a functional which allows to query the current operator version. Must not be + * {@code null} and must not return null */ public ParameterTypeExpression(final String key, String description, OperatorVersionCallable operatorVersion) { this(key, description, null, false, operatorVersion); @@ -125,25 +131,23 @@ public ParameterTypeExpression(final String key, String description, InputPort i } public ParameterTypeExpression(final String key, String description, final InputPort inPort, boolean optional) { - this(key, description, inPort, optional, new Callable() { - - @Override - public OperatorVersion call() throws Exception { - if (inPort != null) { - return inPort.getPorts().getOwner().getOperator().getCompatibilityLevel(); - } else { - - // callers that do not provide an input port are not be able to use the - // expression parser functions - return new OperatorVersion(6, 4, 0); - } - } - }); + this(key, description, inPort, optional, getOperatorVersionProvider(inPort)); if (inPort == null) { LogService.getRoot().log(Level.INFO, "com.rapidminer.parameter.ParameterTypeExpression.no_input_port_provided"); } } + /** @since 9.3 */ + private static Callable getOperatorVersionProvider(InputPort inPort) { + if (inPort != null) { + Operator operator = inPort.getPorts().getOwner().getOperator(); + return operator::getCompatibilityLevel; + } + // callers that do not provide an input port are not able to use the + // expression parser functions => use corresponding compatibility level + return () -> ExpressionParserBuilder.OLD_EXPRESSION_PARSER_FUNCTIONS; + } + private ParameterTypeExpression(final String key, String description, InputPort inPort, boolean optional, Callable operatorVersion) { super(key, description, optional); @@ -210,6 +214,29 @@ public String substitutePredefinedMacros(String parameterValue, Operator operato } else { return parameterValue; } + } + @Override + public String notifyOperatorRenaming(String oldName, String newName, String value) { + if (!value.contains(PARAMETER_VALUE_FUNCTION_NAME) || !value.contains(oldName)) { + return value; + } + Pattern pattern = Pattern.compile(String.format(RENAMING_PATTERN, Pattern.quote(oldName))); + Matcher matcher = pattern.matcher(value); + int pos = 0; + StringBuilder newValue = new StringBuilder(); + while (matcher.find(pos)) { + int start = matcher.start(1); + // constant part before the first match or after previous match + if (start > pos) { + newValue.append(value.substring(pos, start)); + } + newValue.append(newName); + pos = matcher.end(1); + } + if (pos > 0 && pos < value.length()) { + newValue.append(value.substring(pos)); + } + return newValue.toString(); } } diff --git a/src/main/java/com/rapidminer/parameter/ParameterTypeInnerOperator.java b/src/main/java/com/rapidminer/parameter/ParameterTypeInnerOperator.java index afd04c70c..99a068a66 100644 --- a/src/main/java/com/rapidminer/parameter/ParameterTypeInnerOperator.java +++ b/src/main/java/com/rapidminer/parameter/ParameterTypeInnerOperator.java @@ -64,6 +64,7 @@ public boolean isNumerical() { return false; } + /** @return the new operator name if the old value matched the old operator name */ @Override public String notifyOperatorRenaming(String oldOperatorName, String newOperatorName, String parameterValue) { if (oldOperatorName.equals(parameterValue)) { diff --git a/src/main/java/com/rapidminer/parameter/ParameterTypeList.java b/src/main/java/com/rapidminer/parameter/ParameterTypeList.java index 61121d9bd..5f4371243 100644 --- a/src/main/java/com/rapidminer/parameter/ParameterTypeList.java +++ b/src/main/java/com/rapidminer/parameter/ParameterTypeList.java @@ -18,9 +18,12 @@ */ package com.rapidminer.parameter; +import java.util.Arrays; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.function.BiFunction; +import java.util.stream.Collectors; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -211,54 +214,37 @@ public String toString(Object value) { } public static String transformList2String(List parameterList) { - StringBuffer result = new StringBuffer(); - Iterator i = parameterList.iterator(); - boolean first = true; - while (i.hasNext()) { - String[] objects = i.next(); - if (objects.length != 2) { - continue; - } - - String firstToken = objects[0]; - String secondToken = objects[1]; - if (!first) { - result.append(Parameters.RECORD_SEPARATOR); - } - if (secondToken != null) { - if (firstToken != null) { - result.append(firstToken); - } - result.append(Parameters.PAIR_SEPARATOR); - if (secondToken != null) { - result.append(secondToken); - } - } - first = false; - } - return result.toString(); + return parameterList.stream().map(vals -> Arrays.stream(vals) + .collect(Collectors.joining(Character.toString(Parameters.PAIR_SEPARATOR)))) + .collect(Collectors.joining(Character.toString(Parameters.RECORD_SEPARATOR))); } public static List transformString2List(String listString) { - List result = new LinkedList<>(); - String[] splittedList = listString.split(Character.valueOf(Parameters.RECORD_SEPARATOR).toString()); - for (String record : splittedList) { - if (record.length() > 0) { - String[] pair = record.split(Character.valueOf(Parameters.PAIR_SEPARATOR).toString()); - if (pair.length == 2 && pair[0].length() > 0 && pair[1].length() > 0) { - result.add(new String[] { pair[0], pair[1] }); - } - } - } - return result; + return Arrays.stream(listString.split(Character.toString(Parameters.RECORD_SEPARATOR))) + .filter(record -> record.length() > 0) + .map(record -> record.split(Character.toString(Parameters.PAIR_SEPARATOR))) + .filter(pair -> pair.length == 2 && !pair[0].isEmpty() && !pair[1].isEmpty()) + .collect(Collectors.toList()); } + /** @return the changed value after all pairs were notified */ @Override public String notifyOperatorRenaming(String oldOperatorName, String newOperatorName, String parameterValue) { + return notifyOperatorRenamingReplacing((t, v) -> t.notifyOperatorRenaming(oldOperatorName, newOperatorName, v), parameterValue); + } + + /** @return the changed value after all pairs were notified */ + @Override + public String notifyOperatorReplacing(String oldName, Operator oldOp, String newName, Operator newOp, String parameterValue) { + return notifyOperatorRenamingReplacing((t, v) -> t.notifyOperatorReplacing(oldName, oldOp, newName, newOp, v), parameterValue); + } + + /** @since 9.3 */ + private String notifyOperatorRenamingReplacing(BiFunction replacer, String parameterValue) { List list = transformString2List(parameterValue); for (String[] pair : list) { - pair[0] = keyType.notifyOperatorRenaming(oldOperatorName, newOperatorName, pair[0]); - pair[1] = valueType.notifyOperatorRenaming(oldOperatorName, newOperatorName, pair[1]); + pair[0] = replacer.apply(keyType, pair[0]); + pair[1] = replacer.apply(valueType, pair[1]); } return transformList2String(list); } diff --git a/src/main/java/com/rapidminer/parameter/ParameterTypeOperatorParameterTupel.java b/src/main/java/com/rapidminer/parameter/ParameterTypeOperatorParameterTupel.java new file mode 100644 index 000000000..d8b81635d --- /dev/null +++ b/src/main/java/com/rapidminer/parameter/ParameterTypeOperatorParameterTupel.java @@ -0,0 +1,113 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.parameter; + +import org.apache.commons.lang.StringUtils; +import org.w3c.dom.Element; + +import com.rapidminer.operator.Operator; +import com.rapidminer.tools.XMLException; + + +/** + * A specialized {@link ParameterTypeTupel} that handles one {@link Operator} and one of its {@link ParameterType parameters}. + * This will validate that an operator/parameter pair actually exists when + * {@link #notifyOperatorReplacing(String, Operator, String, Operator, String) replacing an operator}. + * + * @author Jan Czogalla + * @since 9.3 + */ +public class ParameterTypeOperatorParameterTupel extends ParameterTypeTupel { + + public static final String PARAMETER_OPERATOR = "operator_name"; + public static final String PARAMETER_PARAMETER = "parameter_name"; + + public ParameterTypeOperatorParameterTupel(Element element) throws XMLException { + super(element); + } + + /** + * Same as {@link #ParameterTypeOperatorParameterTupel(String, String, String, String) + * ParameterTypeOperatorParameterTupel}(key, description, {@value #PARAMETER_OPERATOR}, {@value #PARAMETER_PARAMETER}) + */ + public ParameterTypeOperatorParameterTupel(String key, String description) { + this(key, description, PARAMETER_OPERATOR, PARAMETER_PARAMETER); + } + + /** + * Creates a parameter type that can represent an operator and one of it's parameter keys. + * Will create sub types for the operator of type {@link ParameterTypeInnerOperator} and for the parameter of + * type {@link ParameterTypeString}. The sub types' keys are set according to the metod arguments. + * + * @param key + * the key for this parameter + * @param description + * the description of this parameter + * @param operatorKey + * the key for the operator sub type + * @param parameterKey + * the key for the parameter sub type + * @see ParameterTypeTupel#ParameterTypeTupel(String, String, ParameterType...) + */ + public ParameterTypeOperatorParameterTupel(String key, String description, String operatorKey, String parameterKey) { + super(key, description, new ParameterTypeInnerOperator(operatorKey, "The operator."), + new ParameterTypeString(parameterKey, "The parameter.")); + } + + @Override + public String transformNewValue(String value) { + String transformedValue = super.transformNewValue(value); + String[] tupel = transformString2Tupel(transformedValue); + if (tupel.length != 2 || StringUtils.isEmpty(tupel[0]) || StringUtils.isEmpty(tupel[1])) { + return null; + } + return transformedValue; + } + + /** @return the updated value if this parameter is affected and the new operator has the same parameter; empty string otherwise */ + @Override + public String notifyOperatorReplacing(String oldName, Operator oldOp, String newName, Operator newOp, String parameterValue) { + String[] tupel = transformString2Tupel(parameterValue); + if (!oldName.equals(tupel[0])) { + // parameter is not affected => just return old value + return parameterValue; + } + if (!newOp.getParameters().getKeys().contains(tupel[1])) { + // new operator does not have that parameter => irrelevant + return null; + } + String opValue = getFirstParameterType().notifyOperatorRenaming(oldName, newName, tupel[0]); + if (opValue.equals(tupel[0])) { + // operator name did not change => just return old value + return parameterValue; + } + tupel[0] = opValue; + return transformTupel2String(tupel); + } + + @Override + public Object getDefaultValue() { + return null; + } + + @Override + public String getDefaultValueAsString() { + return null; + } +} diff --git a/src/main/java/com/rapidminer/parameter/ParameterTypeRegexp.java b/src/main/java/com/rapidminer/parameter/ParameterTypeRegexp.java index 0b0bb36f1..81edc0416 100644 --- a/src/main/java/com/rapidminer/parameter/ParameterTypeRegexp.java +++ b/src/main/java/com/rapidminer/parameter/ParameterTypeRegexp.java @@ -28,8 +28,26 @@ */ public class ParameterTypeRegexp extends ParameterTypeString { + /** + * General parameter key for this type of parameter that indicates what to replace + * + * @since 9.3 + */ + public static final String PARAMETER_REPLACE_WHAT = "replace_what"; + + /** + * General parameter key for an accompanying string parameter for the parameter with key {@value #PARAMETER_REPLACE_WHAT} + * that describes the replacement + * + * @since 9.3 + */ + public static final String PARAMETER_REPLACE_BY = "replace_by"; + private static final long serialVersionUID = -4177652183651031337L; + /** @since 9.3 */ + private ParameterTypeString replacementParameter; + public ParameterTypeRegexp(final String key, String description) { this(key, description, true); } @@ -55,4 +73,27 @@ public Collection getPreviewList() { return null; } + /** + * Set the {@link ParameterTypeString} that might be linked to the replacement field + * in the {@link com.rapidminer.gui.properties.RegexpPropertyDialog}. + * + * @param replacementParameter + * the parameter linked to replacement; can be {@code null} + * @since 9.3 + */ + public void setReplacementParameter(ParameterTypeString replacementParameter) { + this.replacementParameter = replacementParameter; + } + + /** + * Returns the {@link ParameterTypeString} linked to the replacement field + * in the {@link com.rapidminer.gui.properties.RegexpPropertyDialog} if it was set. + * + * @return the parameter linked to replacement; can be {@code null} + * @since 9.3 + */ + public ParameterTypeString getReplacementParameter(){ + return replacementParameter; + } + } diff --git a/src/main/java/com/rapidminer/parameter/ParameterTypeTupel.java b/src/main/java/com/rapidminer/parameter/ParameterTypeTupel.java index 5474dc29b..5cd618e3b 100644 --- a/src/main/java/com/rapidminer/parameter/ParameterTypeTupel.java +++ b/src/main/java/com/rapidminer/parameter/ParameterTypeTupel.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.List; +import java.util.function.BiFunction; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -203,7 +204,7 @@ public static String[] transformString2Tupel(String parameterValue) { while (split.size() < 2) { split.add(null); } - return split.toArray(new String[split.size()]); + return split.toArray(new String[0]); } public static String transformTupel2String(String firstValue, String secondValue) { @@ -228,11 +229,23 @@ public static String transformTupel2String(String[] tupel) { return builder.toString(); } + /** @return the changed value after all tupel entries were notified */ @Override public String notifyOperatorRenaming(String oldOperatorName, String newOperatorName, String parameterValue) { + return notifyOperatorRenamingReplacing((t, v) -> t.notifyOperatorRenaming(oldOperatorName, newOperatorName, v), parameterValue); + } + + /** @return the changed value after all tupel entries were notified */ + @Override + public String notifyOperatorReplacing(String oldName, Operator oldOp, String newName, Operator newOp, String parameterValue) { + return notifyOperatorRenamingReplacing((t, v) -> t.notifyOperatorReplacing(oldName, oldOp, newName, newOp, v), parameterValue); + } + + /** @since 9.3 */ + private String notifyOperatorRenamingReplacing(BiFunction replacer, String parameterValue) { String[] tupel = transformString2Tupel(parameterValue); for (int i = 0; i < types.length; i++) { - tupel[i] = types[i].notifyOperatorRenaming(oldOperatorName, newOperatorName, tupel[i]); + tupel[i] = replacer.apply(types[i], tupel[i]); } return transformTupel2String(tupel); } diff --git a/src/main/java/com/rapidminer/parameter/ParameterTypeValue.java b/src/main/java/com/rapidminer/parameter/ParameterTypeValue.java index d127b61be..5dc0869df 100644 --- a/src/main/java/com/rapidminer/parameter/ParameterTypeValue.java +++ b/src/main/java/com/rapidminer/parameter/ParameterTypeValue.java @@ -18,10 +18,12 @@ */ package com.rapidminer.parameter; -import com.rapidminer.tools.XMLException; +import java.util.function.Predicate; import org.w3c.dom.Element; +import com.rapidminer.operator.Operator; +import com.rapidminer.tools.XMLException; /** * This parameter type allows to select either Operator Values or Parameters for logging purposes. @@ -107,13 +109,29 @@ public boolean isNumerical() { return false; } + /** @return the updated selection value if the originally associated operator was the one that was renamed */ @Override public String notifyOperatorRenaming(String oldOperatorName, String newOperatorName, String parameterValue) { + return notifyOperatorRenamingReplacing(oldOperatorName, newOperatorName, s -> true, parameterValue); + } + + /** @return the updated selection value if the new operator still fits the value/parameter; unspecified value/parameter otherwise */ + @Override + public String notifyOperatorReplacing(String oldName, Operator oldOp, String newName, Operator newOp, String parameterValue) { + // only set new value if the corresponding new operator has that value/parameter; otherwise set the value/parameter to null + Predicate validation = s -> s.isValue && newOp.getValue(s.valueParameterName) != null + || !s.isValue && newOp.getParameters().getKeys().contains(s.valueParameterName); + return notifyOperatorRenamingReplacing(oldName, newName, validation, parameterValue); + } + + /** @since 9.3 */ + private String notifyOperatorRenamingReplacing(String oldName, String newName, Predicate validation, String parameterValue) { OperatorValueSelection selection = transformString2OperatorValueSelection(parameterValue); - if (selection != null) { - if (selection.getOperator().equals(oldOperatorName)) { - selection.operatorName = newOperatorName; + if (selection != null && selection.getOperator().equals(oldName)) { + if (!validation.test(selection)) { + selection.valueParameterName = null; } + selection.operatorName = newName; return transformOperatorValueSelection2String(selection); } return parameterValue; diff --git a/src/main/java/com/rapidminer/parameter/Parameters.java b/src/main/java/com/rapidminer/parameter/Parameters.java index 12373151b..559edae9c 100644 --- a/src/main/java/com/rapidminer/parameter/Parameters.java +++ b/src/main/java/com/rapidminer/parameter/Parameters.java @@ -18,22 +18,26 @@ */ package com.rapidminer.parameter; -import com.rapidminer.tools.AbstractObservable; -import com.rapidminer.tools.LogService; -import com.rapidminer.tools.Observer; -import com.rapidminer.tools.Tools; - import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; +import java.util.function.BiFunction; import java.util.logging.Level; import org.w3c.dom.Document; import org.w3c.dom.Element; +import com.rapidminer.operator.Operator; +import com.rapidminer.tools.AbstractObservable; +import com.rapidminer.tools.LogService; +import com.rapidminer.tools.Observer; +import com.rapidminer.tools.Tools; + /** * This class is a collection of the parameter values of a single operator. Instances of @@ -307,14 +311,41 @@ public static List transformString2List(String listString) { return ParameterTypeList.transformString2List(listString); } + /** Notify all set parameters of the operator renaming */ public void notifyRenaming(String oldName, String newName) { - for (String key : keyToValueMap.keySet()) { - ParameterType type = keyToTypeMap.get(key); - String value = keyToValueMap.get(key); - if (type != null && value != null) { - keyToValueMap.put(key, type.notifyOperatorRenaming(oldName, newName, value)); + notifyRenameReplace((t, v) -> t.notifyOperatorRenaming(oldName, newName, v)); + } + + /** + * This method is called when the operator given by {@code oldName} (and {@code oldOp} if it is not {@code null}) + * was replaced with the operator described by {@code newName} and {@code newOp}. + * This will inform all set {@link ParameterType parameters} of the replacing. + * + * @param oldName + * the name of the old operator + * @param oldOp + * the old operator; can be {@code null} + * @param newName + * the name of the new operator + * @param newOp + * the new operator; must not be {@code null} + * @see ParameterType#notifyOperatorReplacing(String, Operator, String, Operator, String) + * @since 9.3 + */ + public void notifyReplacing(String oldName, Operator oldOp, String newName, Operator newOp) { + notifyRenameReplace((t, v) -> t.notifyOperatorReplacing(oldName, oldOp, newName, newOp, v)); + } + + /** @since 9.3 */ + private void notifyRenameReplace(BiFunction replacer) { + for (Entry entry : keyToValueMap.entrySet()) { + ParameterType type = keyToTypeMap.get(entry.getKey()); + if (type != null && entry.getValue() != null) { + entry.setValue(replacer.apply(type, entry.getValue())); } } + keyToValueMap.values().removeIf(Objects::isNull); + fireUpdate(); } /** Renames a parameter, e.g. during importing old XML process files. */ diff --git a/src/main/java/com/rapidminer/parameter/SimpleListBasedParameterHandler.java b/src/main/java/com/rapidminer/parameter/SimpleListBasedParameterHandler.java index 5af24c71e..eb24c7f60 100644 --- a/src/main/java/com/rapidminer/parameter/SimpleListBasedParameterHandler.java +++ b/src/main/java/com/rapidminer/parameter/SimpleListBasedParameterHandler.java @@ -44,7 +44,7 @@ public abstract class SimpleListBasedParameterHandler implements ParameterHandle @Override public String getParameter(String key) throws UndefinedParameterError { - return this.parameters.getParameter(key); + return getParameters().getParameter(key); } @Override @@ -169,12 +169,12 @@ public boolean isParameterSet(String key) throws UndefinedParameterError { @Override public void setListParameter(String key, List list) { - this.parameters.setParameter(key, ParameterTypeList.transformList2String(list)); + getParameters().setParameter(key, ParameterTypeList.transformList2String(list)); } @Override public void setParameter(String key, String value) { - this.parameters.setParameter(key, value); + getParameters().setParameter(key, value); } @Override @@ -196,7 +196,7 @@ public Parameters getParameters() { * @since 9.1 */ private long getParameterAsIntegerType(String key, Function parser) throws UndefinedParameterError { - ParameterType type = this.parameters.getParameterType(key); + ParameterType type = getParameters().getParameterType(key); String value = getParameter(key); if (type instanceof ParameterTypeCategory) { try { diff --git a/src/main/java/com/rapidminer/repository/BlobEntry.java b/src/main/java/com/rapidminer/repository/BlobEntry.java index 3c20b31d4..7951ace91 100644 --- a/src/main/java/com/rapidminer/repository/BlobEntry.java +++ b/src/main/java/com/rapidminer/repository/BlobEntry.java @@ -30,6 +30,7 @@ public interface BlobEntry extends DataEntry { String TYPE_NAME = "blob"; + String BLOB_SUFFIX = ".blob"; @Override default String getType() { diff --git a/src/main/java/com/rapidminer/repository/ConnectionEntry.java b/src/main/java/com/rapidminer/repository/ConnectionEntry.java new file mode 100644 index 000000000..5a7219cc7 --- /dev/null +++ b/src/main/java/com/rapidminer/repository/ConnectionEntry.java @@ -0,0 +1,71 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.repository; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.tools.PasswordInputCanceledException; + + +/** + * General interface for connection entries. + * + * @author Andreas Timm + * @since 9.3 + */ +public interface ConnectionEntry extends IOObjectEntry { + + String TYPE_NAME = "connection"; + // file extensions + String CON_SUFFIX = ".conninfo"; + String CON_MD_SUFFIX = ".connmd"; + + @Override + default String getType() { + return TYPE_NAME; + } + + /** + * Storing the {@link ConnectionInformation} associated with this entry. + * + * @param connectionInformation + * an up-to-date {@link ConnectionInformation} object + * @throws RepositoryException + * if storing did not succeed preserve some information for the user + */ + void storeConnectionInformation(ConnectionInformation connectionInformation) throws RepositoryException; + + /** + * Returns {@code true} if the connection is editable + *

      Warning: Calling this method might be very expensive, only call if really necessary.

      + * + * @return {@code true} if the connection is editable + */ + default boolean isEditable() throws RepositoryException, PasswordInputCanceledException { + return !isReadOnly(); + } + + /** + * Returns the cached {@link com.rapidminer.connection.configuration.ConnectionConfiguration#getType connection type} + * + *

      This method does not block, but return {@code null} if the connection type is not yet known.

      + * + * @return the {@link com.rapidminer.connection.configuration.ConnectionConfiguration#getType connection type}, or {@code null} + */ + String getConnectionType(); +} diff --git a/src/main/java/com/rapidminer/repository/Entry.java b/src/main/java/com/rapidminer/repository/Entry.java index 0c720ba51..fb0ab7008 100644 --- a/src/main/java/com/rapidminer/repository/Entry.java +++ b/src/main/java/com/rapidminer/repository/Entry.java @@ -34,6 +34,8 @@ */ public interface Entry { + String PROPERTIES_SUFFIX = ".properties"; + /** Returns the name, the last part of the location. */ String getName(); diff --git a/src/main/java/com/rapidminer/repository/EntryCreator.java b/src/main/java/com/rapidminer/repository/EntryCreator.java new file mode 100644 index 000000000..e00c48dda --- /dev/null +++ b/src/main/java/com/rapidminer/repository/EntryCreator.java @@ -0,0 +1,73 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.repository; + +/** + * A functional interface to streamline {@link DataEntry} creation in repositories + * + * @param + * the type of the location + * @param + * the subtype of {@link DataEntry} that will be created + * @param + * the subtype of {@link Folder} that the new entry will live in + * @param + * the subtype of {@link Repository} that the new entry will live in + * @author Jan Czogalla + * @since 9.3 + */ +@FunctionalInterface +public interface EntryCreator { + + /** + * Create a new entry with the given location information, parent folder and enclosing repository. + * + * @param location + * the location information needed for the new entry + * @param folder + * the containing folder for the new entry + * @param repository + * the repository the new entry will reside in + * @return the new entry, never {@code null} + * @throws RepositoryException + * if an error occurs + */ + E create(L location, F folder, R repository) throws RepositoryException; + + /** An empty creator, always returning {@code null}. */ + EntryCreator NULL_CREATOR = (l, f, r) -> null; + + /** + * Returns a creator that always creates a {@code null} {@link Entry}. + * + *

      This example illustrates the type-safe way to obtain a {@code null} creator: + *

      +	 *     EntryCreator<L, E, F, R> nulLCreator = EntryCreator.nullCreator();
      +	 * 
      + * + * @param + * subclass of the {@link EntryCreator} to be returned + * @return a creator that always creates a {@code null} {@link Entry}. + * @see #NULL_CREATOR + */ + @SuppressWarnings("unchecked") + static > C nullCreator() { + return (C) NULL_CREATOR; + } +} diff --git a/src/main/java/com/rapidminer/repository/Folder.java b/src/main/java/com/rapidminer/repository/Folder.java index d91fa4b28..de20a4f01 100644 --- a/src/main/java/com/rapidminer/repository/Folder.java +++ b/src/main/java/com/rapidminer/repository/Folder.java @@ -1,45 +1,95 @@ /** * Copyright (C) 2001-2019 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.repository; +import java.util.List; + +import com.rapidminer.connection.ConnectionInformation; import com.rapidminer.operator.IOObject; import com.rapidminer.operator.Operator; +import com.rapidminer.tools.I18N; import com.rapidminer.tools.ProgressListener; -import java.util.List; - /** * An entry containing sub-entries. - * + * * @author Simon Fischer - * */ public interface Folder extends Entry { + /** + * The name of the special connections folder. + *

      To get the connections folder for a repository, use {@link RepositoryTools#getConnectionFolder(Repository)}. + *

      + */ + String CONNECTION_FOLDER_NAME = "Connections"; + String TYPE_NAME = "folder"; + /** + * Error message for creating non-connection entries in this folder. + */ + String MESSAGE_CONNECTION_FOLDER = I18N.getErrorMessage("repository.create_connection_folder"); + + /** + * Error message for creating connection entries outside connection folders. + */ + String MESSAGE_CONNECTION_CREATION = I18N.getErrorMessage("repository.connection_create_outside"); + + /** + * Error message for moving, deleting or renaming connection folders. + */ + String MESSAGE_CONNECTION_FOLDER_CHANGE = I18N.getErrorMessage("repository.connection_folder_change"); + + /** + * Error message for trying to create another connections folder. + */ + String MESSAGE_CONNECTION_FOLDER_DUPLICATE = I18N.getErrorMessage("repository.connection_folder_duplicate"); + + /** + * Error message when trying to create/copy a connection entry to a folder implementation which does not know about connection entries. + */ + String MESSAGE_CONNECTION_FOLDER_CONNECTIONS_UNKNOWN = I18N.getErrorMessage("repository.connection_folder_unknown"); + @Override default String getType() { return TYPE_NAME; } + /** + * Checks whether this folder is the special "Connections" folder in which only connections can be stored. By + * default, the check is case sensitive! Override if it needs to be case-insensitive for special repositories! + * + * @return {@code true} if this folder is called 'Connections' in exactly this capitalization; {@code false} + * otherwise + */ + default boolean isSpecialConnectionsFolder() { + Folder containingFolder = getContainingFolder(); + // folder is one step below the repository + return containingFolder instanceof Repository + // and repo supports connections + && ((Repository) containingFolder).supportsConnections() + // and has the special name + && isConnectionsFolderName(getName(), true); + } + List getDataEntries() throws RepositoryException; List getSubfolders() throws RepositoryException; @@ -55,13 +105,45 @@ IOObjectEntry createIOObjectEntry(String name, IOObject ioobject, Operator calli ProcessEntry createProcessEntry(String name, String processXML) throws RepositoryException; + /** + * Create a {@link ConnectionEntry} with the given name and store the {@link ConnectionInformation}. + * + * @param name + * of the connection entry + * @param connectionInformation + * the information to be stored in this {@link Entry} + * @return the created {@link Entry} + * @throws RepositoryException + * in case storing was not successful + * @see Repository#supportsConnections() + * @since 9.3.0 + */ + default ConnectionEntry createConnectionEntry(String name, ConnectionInformation connectionInformation) throws RepositoryException { + throw new RepositoryConnectionsNotSupportedException(MESSAGE_CONNECTION_FOLDER_CONNECTIONS_UNKNOWN); + } + BlobEntry createBlobEntry(String name) throws RepositoryException; /** * Returns true iff a child with the given name exists and a {@link #refresh()} would find this * entry (or it is already loaded). - * + * * @throws RepositoryException */ boolean canRefreshChild(String childName) throws RepositoryException; + + /** + * Checks if the name is a special "Connections" folder name, i.e. it is the same as {@value + * #CONNECTION_FOLDER_NAME}. Case sensitivity is a parameter. + * + * @param name + * the name to check + * @param caseSensitive + * if {@code true}, the name must be exactly equal; otherwise the case is ignored + * @return whether the name is the special connections folder name + */ + static boolean isConnectionsFolderName(String name, boolean caseSensitive) { + return caseSensitive ? CONNECTION_FOLDER_NAME.equals(name) : CONNECTION_FOLDER_NAME.equalsIgnoreCase(name); + } + } diff --git a/src/main/java/com/rapidminer/repository/IOObjectEntry.java b/src/main/java/com/rapidminer/repository/IOObjectEntry.java index e74f064cd..b59eee98e 100644 --- a/src/main/java/com/rapidminer/repository/IOObjectEntry.java +++ b/src/main/java/com/rapidminer/repository/IOObjectEntry.java @@ -30,6 +30,8 @@ public interface IOObjectEntry extends DataEntry { String TYPE_NAME = "data"; + String MD_SUFFIX = ".md"; + String IOO_SUFFIX = ".ioo"; @Override default String getType() { diff --git a/src/main/java/com/rapidminer/repository/PersistentContentMapperStore.java b/src/main/java/com/rapidminer/repository/PersistentContentMapperStore.java index 806c3b633..9d23d3812 100644 --- a/src/main/java/com/rapidminer/repository/PersistentContentMapperStore.java +++ b/src/main/java/com/rapidminer/repository/PersistentContentMapperStore.java @@ -272,7 +272,7 @@ public String retrieve(String key, RepositoryLocation location, String additiona throw new IllegalArgumentException("key must not be null or empty!"); } if (location == null && additionalHash == null) { - throw new IllegalArgumentException("location and additionalHash must not be null at the same time!"); + return null; } String pathString = createPath(location, additionalHash); @@ -356,7 +356,7 @@ public String createHash(T t) { } if (generator == null) { - throw new UnsupportedOperationException("No hash generator registered for provided object!"); + return null; } return generator.apply(t); diff --git a/src/main/java/com/rapidminer/repository/ProcessEntry.java b/src/main/java/com/rapidminer/repository/ProcessEntry.java index 8e7f04733..aa327d87a 100644 --- a/src/main/java/com/rapidminer/repository/ProcessEntry.java +++ b/src/main/java/com/rapidminer/repository/ProcessEntry.java @@ -27,6 +27,7 @@ public interface ProcessEntry extends DataEntry { String TYPE_NAME = "process"; + String RMP_SUFFIX = ".rmp"; @Override default String getType() { diff --git a/src/main/java/com/rapidminer/repository/Repository.java b/src/main/java/com/rapidminer/repository/Repository.java index c40da0d7c..079f3c826 100644 --- a/src/main/java/com/rapidminer/repository/Repository.java +++ b/src/main/java/com/rapidminer/repository/Repository.java @@ -21,6 +21,7 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; +import com.rapidminer.connection.ConnectionInformation; import com.rapidminer.repository.gui.RepositoryConfigurationPanel; @@ -62,6 +63,20 @@ public interface Repository extends Folder { /** Returns true if the repository is configurable. In that case, */ boolean isConfigurable(); + /** + * Returns whether this repository can successfully handle the {@link com.rapidminer.connection.ConnectionInformation + * ConnectionInformation} objects introduced with RapidMiner Studio 9.3. + * + * @return {@code true} if this repository supports connections; {@code false} otherwise. By default, returns {@code + * false} + * @see Folder#createConnectionEntry(String, ConnectionInformation) + * @since 9.3.0 + */ + default boolean supportsConnections() { + return false; + } + /** Creates a configuration panel. */ RepositoryConfigurationPanel makeConfigurationPanel(); + } diff --git a/src/main/java/com/rapidminer/repository/RepositoryActionConditionAdditionallyNotConnections.java b/src/main/java/com/rapidminer/repository/RepositoryActionConditionAdditionallyNotConnections.java new file mode 100644 index 000000000..d3b51c91e --- /dev/null +++ b/src/main/java/com/rapidminer/repository/RepositoryActionConditionAdditionallyNotConnections.java @@ -0,0 +1,109 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General + * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses/. + */ +package com.rapidminer.repository; + +import java.util.List; + + +/** + * Checks that the sub-condition is satisfied and additionally the entries are not the special connections folder and + * (if the inside check is activated) not inside the connections folder. + * + * @author Gisa Meier + * @since 9.3 + */ +public class RepositoryActionConditionAdditionallyNotConnections implements RepositoryActionCondition { + + private final RepositoryActionCondition subCondition; + private final boolean checkInside; + + /** + * Creates a new RepositoryActionCondition which can be used to check if the selected {@link Entry}s are fulfilling + * a subcondition and are not the special connections folder. In case {@link #checkInside} is {@code true} the + * action is also forbidden for entries inside the special folder. + * + * @param requiredSelectionTypes + * the required selection types for the subcondition + * @param allowRepositories + * if {@code true} the subcondition is {@link RepositoryActionConditionImplStandard} otherwise it is {@link + * RepositoryActionConditionImplStandardNoRepository} + */ + public RepositoryActionConditionAdditionallyNotConnections(Class[] requiredSelectionTypes, + boolean allowRepositories) { + this(requiredSelectionTypes, allowRepositories, false); + } + + /** + * Creates a new RepositoryActionCondition which can be used to check if the selected {@link Entry}s are fulfilling + * a subcondition and are not the special connections folder. In case {@link #checkInside} is {@code true} the + * action is also forbidden for entries inside the special folder. + * + * @param requiredSelectionTypes + * the required selection types for the subcondition + * @param allowRepositories + * if {@code true} the subcondition is {@link RepositoryActionConditionImplStandard} otherwise it is {@link + * RepositoryActionConditionImplStandardNoRepository} + * @param checkInside + * also forbid the action when being inside the special folder + */ + public RepositoryActionConditionAdditionallyNotConnections(Class[] requiredSelectionTypes, + boolean allowRepositories, boolean checkInside) { + this(allowRepositories ? new RepositoryActionConditionImplStandard(requiredSelectionTypes) : + new RepositoryActionConditionImplStandardNoRepository(requiredSelectionTypes), checkInside); + } + + /** + * Creates a new RepositoryActionCondition which can be used to check if the selected {@link Entry}s are + * fulfilling a sub condition and are not the special connections folder or inside it if the inside check is activated. + */ + public RepositoryActionConditionAdditionallyNotConnections(RepositoryActionCondition subCondition, boolean checkInside) { + this.subCondition = subCondition; + this.checkInside = checkInside; + } + + @Override + public boolean evaluateCondition(List entryList) { + if (!subCondition.evaluateCondition(entryList)) { + return false; + } + + for (Entry givenEntry : entryList) { + + // special folders not allowed + if (givenEntry instanceof Folder && ((Folder) givenEntry).isSpecialConnectionsFolder()) { + return false; + } + + if(checkInside){ + // entries inside special folders not allowed + if (givenEntry instanceof Folder) { + if (RepositoryTools.isInSpecialConnectionsFolder((Folder) givenEntry)) { + return false; + } + } else if (RepositoryTools.isInSpecialConnectionsFolder(givenEntry.getContainingFolder())) { + return false; + } + } + } + + // all conditions have been met, so return true + return true; + } + +} diff --git a/src/main/java/com/rapidminer/repository/RepositoryActionConditionImplStandard.java b/src/main/java/com/rapidminer/repository/RepositoryActionConditionImplStandard.java index 7759c3385..01f4e8834 100644 --- a/src/main/java/com/rapidminer/repository/RepositoryActionConditionImplStandard.java +++ b/src/main/java/com/rapidminer/repository/RepositoryActionConditionImplStandard.java @@ -18,11 +18,11 @@ */ package com.rapidminer.repository; -import com.rapidminer.repository.gui.actions.AbstractRepositoryAction; - import java.util.Arrays; import java.util.List; +import com.rapidminer.repository.gui.actions.AbstractRepositoryAction; + /** * Declares a condition for {@link AbstractRepositoryAction}. If the conditions are met, the action @@ -59,14 +59,26 @@ public RepositoryActionConditionImplStandard(List> requiredSelectionTyp this.requiredSelectionRepositoryTypeList = requiredSelectionRepositoryTypeList; } + /** + * Creates a new RepositoryActionCondition which can be used to check if the selected + * {@link Entry}s meet the given conditions. + * + * @param requiredSelectionTypes + * a list with {@link Entry} types. Each selected {@link Entry} must be of one of the + * types on the list or the condition is not met. + */ + public RepositoryActionConditionImplStandard(Class[] requiredSelectionTypes) { + this(requiredSelectionTypes, new Class[0]); + } + /** * Creates a new RepositoryActionCondition which can be used to check if the selected * {@link Entry}s meet the given conditions. * - * @param requiredSelectionTypeList + * @param requiredSelectionTypes * a list with {@link Entry} types. Each selected {@link Entry} must be of one of the * types on the list or the condition is not met. - * @param requiredSelectionRepositoryTypeList + * @param requiredSelectionRepositoryTypes * a list with {@link Repository} types. Each selected {@link Entry} must be of the * types on the list or the condition is not met. */ diff --git a/src/main/java/com/rapidminer/repository/RepositoryActionConditionImplStandardNoRepository.java b/src/main/java/com/rapidminer/repository/RepositoryActionConditionImplStandardNoRepository.java index 669a63766..2c6337473 100644 --- a/src/main/java/com/rapidminer/repository/RepositoryActionConditionImplStandardNoRepository.java +++ b/src/main/java/com/rapidminer/repository/RepositoryActionConditionImplStandardNoRepository.java @@ -45,14 +45,26 @@ public RepositoryActionConditionImplStandardNoRepository(List> required super(requiredSelectionTypeList, requiredSelectionRepositoryTypeList); } + /** + * Creates a new RepositoryActionCondition which can be used to check if the selected + * {@link Entry}s meet the given conditions. + * + * @param requiredSelectionTypes + * a list with {@link Entry} types. Each selected {@link Entry} must be of one of the + * types on the list or the condition is not met. + */ + public RepositoryActionConditionImplStandardNoRepository(Class[] requiredSelectionTypes) { + this(requiredSelectionTypes, new Class[0]); + } + /** * Creates a new RepositoryActionCondition which can be used to check if the selected * {@link Entry}s meet the given conditions. * - * @param requiredSelectionTypeList + * @param requiredSelectionTypes * a list with {@link Entry} types. Each selected {@link Entry} must be of one of the * types on the list or the condition is not met. - * @param requiredSelectionRepositoryTypeList + * @param requiredSelectionRepositoryTypes * a list with {@link Repository} types. Each selected {@link Entry} must be of the * types on the list or the condition is not met. */ diff --git a/src/main/java/com/rapidminer/repository/RepositoryActionConditionRepositoryAndConnections.java b/src/main/java/com/rapidminer/repository/RepositoryActionConditionRepositoryAndConnections.java new file mode 100644 index 000000000..e5197e1e0 --- /dev/null +++ b/src/main/java/com/rapidminer/repository/RepositoryActionConditionRepositoryAndConnections.java @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General + * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses/. + */ +package com.rapidminer.repository; + +import java.util.List; + + +/** + * Checks that the sub-condition is satisfied and additionally the entry is either a repository that supports connections, or the special Connections folder. + * + * @author Marco Boeck + * @since 9.3.0 + */ +public class RepositoryActionConditionRepositoryAndConnections implements RepositoryActionCondition { + + private final RepositoryActionCondition subCondition; + + /** + * Creates a new RepositoryActionCondition which can be used to check if the selected {@link Entry}s are fulfilling + * a subcondition and are either a repository or the special connections folder. + * + * @param requiredSelectionTypes + * the required selection types for the subcondition + */ + public RepositoryActionConditionRepositoryAndConnections(Class[] requiredSelectionTypes) { + this(new RepositoryActionConditionImplStandard(requiredSelectionTypes)); + } + + /** + * Creates a new RepositoryActionCondition which can be used to check if the selected {@link Entry}s are + * fulfilling a sub condition and are not the special connections folder or inside it if the inside check is activated. + */ + public RepositoryActionConditionRepositoryAndConnections(RepositoryActionCondition subCondition) { + this.subCondition = subCondition; + } + + @Override + public boolean evaluateCondition(List entryList) { + if (!subCondition.evaluateCondition(entryList)) { + return false; + } + + for (Entry givenEntry : entryList) { + + if (givenEntry instanceof Repository) { + if (!((Repository) givenEntry).supportsConnections()) { + // if the repository does not support connections, we can abort straight away + return false; + } + } else if (!((Folder) givenEntry).isSpecialConnectionsFolder()) { + // likewise if the folder is not the special Connections folder, we can also abort + return false; + } + } + + // all conditions have been met, so return true + return true; + } + +} diff --git a/src/main/java/com/rapidminer/repository/RepositoryConnectionsFolderDuplicateException.java b/src/main/java/com/rapidminer/repository/RepositoryConnectionsFolderDuplicateException.java new file mode 100644 index 000000000..f9ce48d23 --- /dev/null +++ b/src/main/java/com/rapidminer/repository/RepositoryConnectionsFolderDuplicateException.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. +*/ +package com.rapidminer.repository; + +/** + * Thrown if trying to create another connections folder. + * + * @author Marco Boeck + * @since 9.3.0 + */ +public class RepositoryConnectionsFolderDuplicateException extends RepositoryException { + + + public RepositoryConnectionsFolderDuplicateException() {} + + public RepositoryConnectionsFolderDuplicateException(String message) { + super(message); + } + + public RepositoryConnectionsFolderDuplicateException(Throwable cause) { + super(cause); + } + + public RepositoryConnectionsFolderDuplicateException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/com/rapidminer/repository/RepositoryConnectionsFolderImmutableException.java b/src/main/java/com/rapidminer/repository/RepositoryConnectionsFolderImmutableException.java new file mode 100644 index 000000000..24618c038 --- /dev/null +++ b/src/main/java/com/rapidminer/repository/RepositoryConnectionsFolderImmutableException.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. +*/ +package com.rapidminer.repository; + +/** + * Thrown if trying to change a special connections folder. This includes moving it, renaming it, or trying to delete it. + * + * @author Marco Boeck + * @since 9.3.0 + */ +public class RepositoryConnectionsFolderImmutableException extends RepositoryException { + + + public RepositoryConnectionsFolderImmutableException() {} + + public RepositoryConnectionsFolderImmutableException(String message) { + super(message); + } + + public RepositoryConnectionsFolderImmutableException(Throwable cause) { + super(cause); + } + + public RepositoryConnectionsFolderImmutableException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/com/rapidminer/repository/RepositoryConnectionsNotSupportedException.java b/src/main/java/com/rapidminer/repository/RepositoryConnectionsNotSupportedException.java new file mode 100644 index 000000000..8110bbeb5 --- /dev/null +++ b/src/main/java/com/rapidminer/repository/RepositoryConnectionsNotSupportedException.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. +*/ +package com.rapidminer.repository; + +/** + * Thrown if trying to create a connection inside a repository which does not support connections. + * + * @author Marco Boeck + * @since 9.3.0 + */ +public class RepositoryConnectionsNotSupportedException extends RepositoryException { + + + public RepositoryConnectionsNotSupportedException() {} + + public RepositoryConnectionsNotSupportedException(String message) { + super(message); + } + + public RepositoryConnectionsNotSupportedException(Throwable cause) { + super(cause); + } + + public RepositoryConnectionsNotSupportedException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/com/rapidminer/repository/RepositoryEntryNotFoundException.java b/src/main/java/com/rapidminer/repository/RepositoryEntryNotFoundException.java index 8531db9cf..768c35099 100644 --- a/src/main/java/com/rapidminer/repository/RepositoryEntryNotFoundException.java +++ b/src/main/java/com/rapidminer/repository/RepositoryEntryNotFoundException.java @@ -18,6 +18,8 @@ */ package com.rapidminer.repository; +import com.rapidminer.tools.I18N; + /** * Thrown if a repository entry cannot be found. * @@ -33,6 +35,11 @@ public RepositoryEntryNotFoundException(String message) { super(message); } + /** @since 9.3 */ + public RepositoryEntryNotFoundException(RepositoryLocation location) { + this(I18N.getErrorMessage("repository.error.non_existent_entry", location)); + } + public RepositoryEntryNotFoundException(Throwable cause) { super(cause); } diff --git a/src/main/java/com/rapidminer/repository/RepositoryEntryWrongTypeException.java b/src/main/java/com/rapidminer/repository/RepositoryEntryWrongTypeException.java index 79f5135dd..d06fcd8f4 100644 --- a/src/main/java/com/rapidminer/repository/RepositoryEntryWrongTypeException.java +++ b/src/main/java/com/rapidminer/repository/RepositoryEntryWrongTypeException.java @@ -18,6 +18,8 @@ */ package com.rapidminer.repository; +import com.rapidminer.tools.I18N; + /** * Thrown if a repository entry was of the wrong type. * @@ -33,6 +35,11 @@ public RepositoryEntryWrongTypeException(String message) { super(message); } + /** @since 9.3 */ + public RepositoryEntryWrongTypeException(RepositoryLocation location, String expectedType, String actualType) { + this(I18N.getErrorMessage("repository.error.mismatched_entry_type", location, expectedType, actualType)); + } + public RepositoryEntryWrongTypeException(Throwable cause) { super(cause); } diff --git a/src/main/java/com/rapidminer/repository/RepositoryFilter.java b/src/main/java/com/rapidminer/repository/RepositoryFilter.java new file mode 100644 index 000000000..b2c19626d --- /dev/null +++ b/src/main/java/com/rapidminer/repository/RepositoryFilter.java @@ -0,0 +1,65 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.repository; + +import java.util.List; + + +/** + * This filter can be added to the {@link RepositoryManager} to show only a subset of all the {@link Repository Repositories} + * + * @author Andreas Timm + * @since 9.3 + */ +public interface RepositoryFilter { + + /** + * Will be called when the {@link RepositoryManager}'s save method is invoked + */ + void save(); + + /** + * The actual filtering happens here. + * This method will be called every time the {@link com.rapidminer.repository.gui.RepositoryTree} is shown + * + * @param repositories + * list of repositories, may be smaller than the original set already due to other applied filters + * @return the filtered list of repositories, never {@code null} + */ + List filter(List repositories); + + /** + * Reset this filter to show all the available entries. This can be called from RapidMiner in case a filtered-out + * {@link Repository} hides a requested element. I.e. "show location of current process" needs to show the containing + * {@link Repository} in the {@link com.rapidminer.repository.gui.RepositoryBrowser}. This reset needs to make sure + * that this {@link RepositoryFilter} does not filter out anything, just like it does not exist. + */ + void reset(); + + /** + * When registering a {@link RepositoryFilter} on the {@link com.rapidminer.repository.gui.RepositoryBrowser} it will + * immediately add a callback through this method so that a change in the filter configuration for instance can use + * it to trigger an update of the {@link com.rapidminer.repository.gui.RepositoryBrowser} UI. + * This should be triggered every time the filter is changed else the changes will not be reflected in the UI. + * + * @param notifyForUpdate + * run this callback to trigger an update of the repository view + */ + void notificationCallback(Runnable notifyForUpdate); +} diff --git a/src/main/java/com/rapidminer/repository/RepositoryLocation.java b/src/main/java/com/rapidminer/repository/RepositoryLocation.java index 07c0a2eb2..f255ad588 100644 --- a/src/main/java/com/rapidminer/repository/RepositoryLocation.java +++ b/src/main/java/com/rapidminer/repository/RepositoryLocation.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Set; +import com.rapidminer.Process; import com.rapidminer.operator.Operator; import com.rapidminer.operator.UserError; @@ -201,8 +202,7 @@ public String getPath() { public Entry locateEntry() throws RepositoryException { Repository repos = getRepository(); if (repos != null) { - Entry entry = repos.locate(getPath()); - return entry; + return repos.locate(getPath()); } else { return null; } @@ -411,7 +411,7 @@ public static List removeIntersectedLocations(List { /** @since 9.0 */ static final List SPECIAL_RESOURCE_REPOSITORY_NAMES = new ArrayList<>(); + static { SPECIAL_RESOURCE_REPOSITORY_NAMES.add(SAMPLE_REPOSITORY_NAME); } + /** @since 9.0.0 */ private static final Map REPOSITORY_ORIGINS = new HashMap<>(); @@ -84,7 +90,7 @@ public class RepositoryManager extends AbstractObservable { /** @since 9.0 */ private static final String LOG_IGNORE_FOLDER_PREFIX = LOG_SAMPLES_PREFIX + "ignore_folder."; /** @since 9.0 */ - private static final String LOG_REGISTER_ERROR_PREFIX = LOG_SAMPLES_PREFIX + "register_error."; + private static final String LOG_REGISTER_ERROR_PREFIX = LOG_SAMPLES_PREFIX + "register_error."; private static RepositoryManager instance; private static final Object INSTANCE_LOCK = new Object(); @@ -102,6 +108,9 @@ public class RepositoryManager extends AbstractObservable { private final EventListenerList listeners = new EventListenerList(); + /** @since 9.3 */ + private final List repositoryFilters = new CopyOnWriteArrayList<>(); + /** * listener which reacts on repository changes like renaming and sorts the list of repositories */ @@ -124,7 +133,6 @@ public void entryChanged(Entry entry) { public void entryAdded(Entry newEntry, Folder parent) {} }; - /** * Ordered types of {@link Repository}s * @@ -473,6 +481,23 @@ public List getRepositories() { return Collections.unmodifiableList(repositories); } + /** + * Returns the filtered, visible list of {@link Repository Repositories} + * + * @return a list of repositories where all filters were applied + */ + public List getFilteredRepositories() { + List result = new ArrayList<>(repositories); + for (RepositoryFilter repositoryFilter : repositoryFilters) { + try { + result = repositoryFilter.filter(result); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "com.rapidminer.repository.RepositoryManager.filter_failure", e); + } + } + return result; + } + /** * Gets a registered ({@link #addRepository(Repository)} repository by * {@link Repository#getName()} @@ -559,6 +584,7 @@ public void createRepositoryIfNoneIsDefined() { */ public void save() { provider.save(getRepositories()); + repositoryFilters.forEach(RepositoryFilter::save); } /** Stores an IOObject at the given location. Creates entries if they don't exist. */ @@ -569,8 +595,13 @@ public IOObject store(IOObject ioobject, RepositoryLocation location, Operator c /** Stores an IOObject at the given location. Creates entries if they don't exist. */ public IOObject store(IOObject ioobject, RepositoryLocation location, Operator callingOperator, - ProgressListener progressListener) throws RepositoryException { + ProgressListener progressListener) throws RepositoryException { Entry entry = location.locateEntry(); + boolean isConnectionEntry = entry instanceof ConnectionEntry; + if (entry != null && isConnectionEntry != ioobject instanceof ConnectionInformationContainerIOObject) { + String expectedType = isConnectionEntry ? "connection" : "data"; + throw new RepositoryEntryWrongTypeException(location, expectedType, entry.getType()); + } if (entry == null) { RepositoryLocation parentLocation = location.parent(); if (parentLocation != null) { @@ -588,16 +619,23 @@ public IOObject store(IOObject ioobject, RepositoryLocation location, Operator c } else { parentFolder = parentLocation.createFoldersRecursively(); } - parentFolder.createIOObjectEntry(childName, ioobject, callingOperator, progressListener); + if (ioobject instanceof ConnectionInformationContainerIOObject) { + parentFolder.createConnectionEntry(childName, ((ConnectionInformationContainerIOObject) ioobject).getConnectionInformation()); + } else { + parentFolder.createIOObjectEntry(childName, ioobject, callingOperator, progressListener); + } return ioobject; } else { - throw new RepositoryException("Entry '" + location + "' does not exist."); + throw new RepositoryEntryNotFoundException(location); } + } else if (isConnectionEntry) { + ((ConnectionEntry) entry).storeConnectionInformation(((ConnectionInformationContainerIOObject) ioobject).getConnectionInformation()); + return ioobject; } else if (entry instanceof IOObjectEntry) { ((IOObjectEntry) entry).storeData(ioobject, callingOperator, progressListener); return ioobject; } else { - throw new RepositoryException("Entry '" + location + "' is not a data entry, but " + entry.getType()); + throw new RepositoryEntryWrongTypeException(location, IOObjectEntry.TYPE_NAME, entry.getType()); } } @@ -622,12 +660,12 @@ public BlobEntry getOrCreateBlob(RepositoryLocation location) throws RepositoryE } return parentFolder.createBlobEntry(childName); } else { - throw new RepositoryException("Entry '" + location + "' does not exist."); + throw new RepositoryEntryNotFoundException(location); } } else if (entry instanceof BlobEntry) { return (BlobEntry) entry; } else { - throw new RepositoryException("Entry '" + location + "' is not a blob entry, but a " + entry.getType()); + throw new RepositoryEntryWrongTypeException(location, BlobEntry.TYPE_NAME, entry.getType()); } } @@ -658,7 +696,7 @@ public void copy(RepositoryLocation source, Folder destination, String newName, * old name will be kept. */ public void copy(RepositoryLocation source, Folder destination, String newName, boolean overwriteIfExists, - ProgressListener listener) throws RepositoryException { + ProgressListener listener) throws RepositoryException { if (listener != null) { listener.setTotal(DEFAULT_TOTAL_PROGRESS); listener.setCompleted(0); @@ -673,7 +711,7 @@ public void copy(RepositoryLocation source, Folder destination, String newName, } private void copy(RepositoryLocation source, Folder destination, String newName, boolean overwriteIfExists, - ProgressListener listener, int minProgress, int maxProgress) throws RepositoryException { + ProgressListener listener, int minProgress, int maxProgress) throws RepositoryException { Entry entry = source.locateEntry(); if (entry == null) { throw new RepositoryException("No such entry: " + source); @@ -682,7 +720,7 @@ private void copy(RepositoryLocation source, Folder destination, String newName, } private void copy(Entry entry, Folder destination, String newName, boolean overwriteIfExists, ProgressListener listener, - int minProgress, int maxProgress) throws RepositoryException { + int minProgress, int maxProgress) throws RepositoryException { if (listener != null) { listener.setMessage(entry.getName()); } @@ -735,7 +773,11 @@ private void copy(Entry entry, Folder destination, String newName, boolean overw if (listener != null) { listener.setCompleted((minProgress + maxProgress) / 2); } - destination.createIOObjectEntry(newName, original, null, null); + if (original instanceof ConnectionInformationContainerIOObject) { + destination.createConnectionEntry(newName, ((ConnectionInformationContainerIOObject) original).getConnectionInformation()); + } else { + destination.createIOObjectEntry(newName, original, null, null); + } if (listener != null) { listener.setCompleted(maxProgress); } @@ -753,7 +795,12 @@ private void copy(Entry entry, Folder destination, String newName, boolean overw throw new RepositoryException(e); } } else if (entry instanceof Folder) { - String sourceAbsolutePath = entry.getLocation().getAbsoluteLocation(); + Folder folder = (Folder) entry; + Folder connectionFolder = RepositoryTools.getConnectionFolder(folder.getLocation().getRepository()); + if (connectionFolder != null && connectionFolder.getLocation().getAbsoluteLocation().equals(folder.getLocation().getAbsoluteLocation())) { + throw new RepositoryConnectionsFolderImmutableException(Folder.MESSAGE_CONNECTION_FOLDER_CHANGE); + } + String sourceAbsolutePath = folder.getLocation().getAbsoluteLocation(); String destinationAbsolutePath = destination.getLocation().getAbsoluteLocation(); // make sure same folder moves are forbidden if (sourceAbsolutePath.equals(destinationAbsolutePath)) { @@ -767,8 +814,8 @@ private void copy(Entry entry, Folder destination, String newName, boolean overw } Folder destinationFolder = destination.createFolder(newName); List allChildren = new LinkedList<>(); - allChildren.addAll(((Folder) entry).getSubfolders()); - allChildren.addAll(((Folder) entry).getDataEntries()); + allChildren.addAll(folder.getSubfolders()); + allChildren.addAll(folder.getDataEntries()); final int count = allChildren.size(); int progressStart = minProgress; int progressDiff = maxProgress - minProgress; @@ -797,7 +844,7 @@ public void move(RepositoryLocation source, Folder destination, String newName, /** Moves an entry to a given destination folder with the name newName. */ public void move(RepositoryLocation source, Folder destination, String newName, boolean overwriteIfExists, - ProgressListener listener) throws RepositoryException { + ProgressListener listener) throws RepositoryException { Entry entry = source.locateEntry(); if (entry == null) { throw new RepositoryException("No such entry: " + source); @@ -1023,23 +1070,82 @@ public void walk(Entry start, RepositoryVisitor visitor, Cl } } + /** + * Add a filter to hide parts of the {@link Repository} + * + * @param filter + * a custom filter + * @since 9.3 + */ + public void registerRepositoryFilter(RepositoryFilter filter) { + if (!RapidMiner.getExecutionMode().isHeadless() && filter != null) { + repositoryFilters.add(filter); + filter.notificationCallback(() -> { + TreeModel treeModel = RapidMinerGUI.getMainFrame().getRepositoryBrowser().getRepositoryTree().getModel(); + if (treeModel instanceof RepositoryTreeModel) { + ((RepositoryTreeModel) treeModel).notifyTreeStructureChanged(); + } + }); + } + } + + /** + * Remove a previously registered filter + * + * @param filter + * to be removed + * @since 9.3 + */ + public void unregisterRepositoryFilter(RepositoryFilter filter) { + repositoryFilters.remove(filter); + } + + /** + * Due to the filters the results for an action that wants to highlight a repository entry may be hidden. + * The {@link com.rapidminer.repository.gui.RepositoryTree} will ask this manager to unhide a repository name. + * + * @param repositoryName + * the name of the repository to show + * @since 9.3 + */ + public void unhide(String repositoryName) { + Repository repository; + try { + repository = getRepository(repositoryName); + } catch (RepositoryException e) { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.repository.RepositoryManager.repository_does_not_exist", e); + return; + } + if (repository != null && !getFilteredRepositories().contains(repository)) { + List singleRepoList = Collections.singletonList(repository); + // reset filters that hide the requested repository + repositoryFilters.stream().filter(filter -> filter.filter(singleRepoList).isEmpty()).forEach( filter -> { + try { + filter.reset(); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "com.rapidminer.repository.RepositoryManager.filter_failure", e); + } + }); + } + } + + /** * Generates new entry name for copies * * @param destination * The folder, to which the entry has to be copied - * @param newName + * @param originalName * The name of the entry, which has to be copied * @return A new, not used entry name * @throws RepositoryException */ - private String getNewNameForExistingEntry(Folder destination, String newName) throws RepositoryException { - String originalName = newName; - newName = "Copy of " + newName; + private String getNewNameForExistingEntry(Folder destination, String originalName) throws RepositoryException { + String newName; int i = 2; - while (destination.containsEntry(newName)) { - newName = "Copy " + i++ + " of " + originalName; - } + do { + newName = originalName + " - " + i++; + } while (destination.containsEntry(newName)); return newName; } diff --git a/src/main/java/com/rapidminer/repository/RepositoryNotConnectionsFolderException.java b/src/main/java/com/rapidminer/repository/RepositoryNotConnectionsFolderException.java new file mode 100644 index 000000000..1f2646cff --- /dev/null +++ b/src/main/java/com/rapidminer/repository/RepositoryNotConnectionsFolderException.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. +*/ +package com.rapidminer.repository; + +/** + * Thrown if trying to store a {@link com.rapidminer.connection.ConnectionInformation} into something else than the + * special Connections folder. This will trigger a "store in Connections folder of this repo instead?" dialog when in GUI mode. + * + * @author Marco Boeck + * @since 9.3.0 + */ +public class RepositoryNotConnectionsFolderException extends RepositoryException { + + + public RepositoryNotConnectionsFolderException() {} + + public RepositoryNotConnectionsFolderException(String message) { + super(message); + } + + public RepositoryNotConnectionsFolderException(Throwable cause) { + super(cause); + } + + public RepositoryNotConnectionsFolderException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/com/rapidminer/repository/RepositoryStoreOtherInConnectionsFolderException.java b/src/main/java/com/rapidminer/repository/RepositoryStoreOtherInConnectionsFolderException.java new file mode 100644 index 000000000..387e29403 --- /dev/null +++ b/src/main/java/com/rapidminer/repository/RepositoryStoreOtherInConnectionsFolderException.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. +*/ +package com.rapidminer.repository; + +/** + * Thrown if trying to store something else than {@link com.rapidminer.connection.ConnectionInformation} into the + * special Connections folder. + * + * @author Marco Boeck + * @since 9.3.0 + */ +public class RepositoryStoreOtherInConnectionsFolderException extends RepositoryException { + + + public RepositoryStoreOtherInConnectionsFolderException() {} + + public RepositoryStoreOtherInConnectionsFolderException(String message) { + super(message); + } + + public RepositoryStoreOtherInConnectionsFolderException(Throwable cause) { + super(cause); + } + + public RepositoryStoreOtherInConnectionsFolderException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/com/rapidminer/repository/RepositoryTools.java b/src/main/java/com/rapidminer/repository/RepositoryTools.java index 6caf19fd6..84111cb6f 100644 --- a/src/main/java/com/rapidminer/repository/RepositoryTools.java +++ b/src/main/java/com/rapidminer/repository/RepositoryTools.java @@ -18,7 +18,10 @@ */ package com.rapidminer.repository; +import java.util.Collections; import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; import com.rapidminer.external.alphanum.AlphanumComparator; import com.rapidminer.external.alphanum.AlphanumComparator.AlphanumCaseSensitivity; @@ -79,6 +82,10 @@ private RepositoryTools() { * less, equal or higher than {@link Entry}2s name. */ public static final Comparator ENTRY_COMPARATOR = (entry1, entry2) -> { + int specialFolderSorting = specialFolderFirst(entry1, entry2); + if (specialFolderSorting != 0) { + return specialFolderSorting; + } Integer nullComparison = compareForNull(entry1, entry2); if (nullComparison != null) { return nullComparison; @@ -99,6 +106,10 @@ private RepositoryTools() { * @since 7.4 */ public static final Comparator ENTRY_COMPARATOR_LAST_MODIFIED = (entry1, entry2) -> { + int specialFolderSorting = specialFolderFirst(entry1, entry2); + if (specialFolderSorting != 0) { + return specialFolderSorting; + } boolean entry1HasDate = entry1 instanceof DateEntry; boolean entry2HasDate = entry2 instanceof DateEntry; // sort entries without modification date to front (i.e. usually folder) @@ -118,6 +129,19 @@ private RepositoryTools() { return ENTRY_COMPARATOR.compare(entry1, entry2); }; + /** + * Sort special folders before all other folders. + */ + private static int specialFolderFirst(Entry entry1, Entry entry2) { + boolean firstIsSpecial = entry1 instanceof Folder && ((Folder) entry1).isSpecialConnectionsFolder(); + boolean secondIsSpecial = entry2 instanceof Folder && ((Folder) entry2).isSpecialConnectionsFolder(); + if (firstIsSpecial) { + return secondIsSpecial ? 0 : -1; + } else { + return secondIsSpecial ? 1 : 0; + } + } + /** * Compares two repositories, ordered by {@link RepositoryType} (Samples, DB, Local * Repositories, Remote Repositories, Others). If the {@link RepositoryType} of the @@ -184,4 +208,60 @@ private static int compareResourceRepositoryNames(String name1, String name2) { } return index2 == -1 ? -1 : index1 - index2; } + + /** + * Checks if the folder is a special connections folder or inside one. + * + * @param folder + * the folder to check + * @return whether the folder is or is in a special folder + */ + public static boolean isInSpecialConnectionsFolder(Folder folder) { + // no parent -> repository + if (folder.getContainingFolder() == null) { + return false; + } + // find super-folder with its super-folder the repository + Folder nextFolder = folder; + while (nextFolder.getContainingFolder().getContainingFolder() != null) { + nextFolder = nextFolder.getContainingFolder(); + } + // check for the special name + return nextFolder.isSpecialConnectionsFolder(); + } + + /** + * Returns the special connections folder for the repository or {@code null}. + * + * @param repository + * the repository for which to retrieve the Connections folder + * @return the connections folder or {@code null} + * @throws RepositoryException + * if a repository exception happens while inspecting the subfolders + */ + public static Folder getConnectionFolder(Repository repository) throws RepositoryException { + return repository.getSubfolders().stream() + .filter(Folder::isSpecialConnectionsFolder).findAny().orElse(null); + } + + /** + * Returns all connections defined for the repository. + * + * @param repository + * the repository for which to retrieve the connections + * @return all {@link ConnectionEntry}s in this repository + * @throws RepositoryException + * if accessing the connections folder fails + */ + public static List getConnections(Repository repository) throws RepositoryException { + Folder connectionFolder = getConnectionFolder(repository); + if (connectionFolder != null) { + return connectionFolder.getDataEntries().stream() + .filter(e -> ConnectionEntry.TYPE_NAME.equals(e.getType())) + .map(e -> (ConnectionEntry) e) + .collect(Collectors.toList()); + } else { + return Collections.emptyList(); + } + } } diff --git a/src/main/java/com/rapidminer/repository/gui/LocalRepositoryPanel.java b/src/main/java/com/rapidminer/repository/gui/LocalRepositoryPanel.java index fea1d13a8..0e4ee15ff 100644 --- a/src/main/java/com/rapidminer/repository/gui/LocalRepositoryPanel.java +++ b/src/main/java/com/rapidminer/repository/gui/LocalRepositoryPanel.java @@ -328,7 +328,7 @@ private void checkConfiguration(File file, String alias) throws RepositoryExcept if (repo instanceof LocalRepository) { if (((LocalRepository) repo).getRoot().equals(file)) { throw new RepositoryException(I18N.getMessage(I18N.getErrorBundle(), - "repository.repository_creation_duplicate_location")); + "repository.repository_creation_duplicate_location", repo.getName())); } } if (repo.getName().equals(alias)) { diff --git a/src/main/java/com/rapidminer/repository/gui/RepositoryBrowser.java b/src/main/java/com/rapidminer/repository/gui/RepositoryBrowser.java index 99076940f..10f9eb7c0 100644 --- a/src/main/java/com/rapidminer/repository/gui/RepositoryBrowser.java +++ b/src/main/java/com/rapidminer/repository/gui/RepositoryBrowser.java @@ -29,6 +29,7 @@ import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JMenu; +import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; @@ -44,6 +45,7 @@ import com.rapidminer.gui.tools.ResourceDockKey; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.gui.tools.components.DropDownPopupButton; +import com.rapidminer.repository.ConnectionEntry; import com.rapidminer.repository.Entry; import com.rapidminer.repository.Folder; import com.rapidminer.repository.IOObjectEntry; @@ -75,6 +77,13 @@ public void loggedActionPerformed(ActionEvent e) {} private final RepositoryTree tree; + /** + * Tracking this {@link JPopupMenu} to be able to add items via {@link RepositoryBrowser#addMenuItem(JMenuItem)} + * + * @since 9.3 + */ + private final JPopupMenu furtherActionsMenu = new JPopupMenu(); + public RepositoryBrowser() { this(null); } @@ -100,6 +109,8 @@ public RepositoryBrowser(DragListener dragListener) { if (entry instanceof ProcessEntry) { RepositoryTree.openProcess((ProcessEntry) entry); + } else if (entry instanceof ConnectionEntry) { + OpenAction.showConnectionInformationDialog((ConnectionEntry) entry); } else if (entry instanceof IOObjectEntry) { OpenAction.showAsResult((IOObjectEntry) entry); } else { @@ -110,7 +121,6 @@ public RepositoryBrowser(DragListener dragListener) { setLayout(new BorderLayout()); - final JPopupMenu furtherActionsMenu = new JPopupMenu(); furtherActionsMenu.add(ADD_REPOSITORY_ACTION); furtherActionsMenu.add(tree.CREATE_FOLDER_ACTION); final JMenu sortActionsMenu = new JMenu(SORT_REPOSITORY_ACTION); @@ -182,4 +192,26 @@ public DockKey getDockKey() { public void expandToRepositoryLocation(RepositoryLocation storedRepositoryLocation) { tree.expandAndSelectIfExists(storedRepositoryLocation); } + + /** + * Add a {@link JMenuItem} to the popup-button in the upper right corner of this RepositoryBrowser + * + * @param item + * the {@link JMenuItem} to be added to this {@link RepositoryBrowser} + * @since 9.3 + */ + public void addMenuItem(JMenuItem item) { + furtherActionsMenu.add(item); + } + + /** + * Remove a {@link JMenuItem} from the popup-button in the upper right corner of this RepositoryBrowser + * + * @param item + * the {@link JMenuItem} to be removed from this {@link RepositoryBrowser} + * @since 9.3 + */ + public void removeMenuItem(JMenuItem item) { + furtherActionsMenu.remove(item); + } } diff --git a/src/main/java/com/rapidminer/repository/gui/RepositoryTree.java b/src/main/java/com/rapidminer/repository/gui/RepositoryTree.java index 3f8d353aa..1f357b552 100644 --- a/src/main/java/com/rapidminer/repository/gui/RepositoryTree.java +++ b/src/main/java/com/rapidminer/repository/gui/RepositoryTree.java @@ -1,18 +1,18 @@ /** * Copyright (C) 2001-2019 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. */ @@ -37,7 +37,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Enumeration; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -77,27 +76,36 @@ import com.rapidminer.gui.tools.components.ToolTipWindow.TooltipLocation; import com.rapidminer.gui.tools.dialogs.ConfirmDialog; import com.rapidminer.gui.tools.dialogs.SelectionDialog; +import com.rapidminer.repository.ConnectionEntry; import com.rapidminer.repository.DataEntry; import com.rapidminer.repository.Entry; import com.rapidminer.repository.Folder; import com.rapidminer.repository.ProcessEntry; import com.rapidminer.repository.Repository; import com.rapidminer.repository.RepositoryActionCondition; +import com.rapidminer.repository.RepositoryActionConditionAdditionallyNotConnections; import com.rapidminer.repository.RepositoryActionConditionImplConfigRepository; import com.rapidminer.repository.RepositoryActionConditionImplStandard; -import com.rapidminer.repository.RepositoryActionConditionImplStandardNoRepository; +import com.rapidminer.repository.RepositoryActionConditionRepositoryAndConnections; +import com.rapidminer.repository.RepositoryConnectionsFolderImmutableException; +import com.rapidminer.repository.RepositoryConnectionsNotSupportedException; import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; import com.rapidminer.repository.RepositoryManager; +import com.rapidminer.repository.RepositoryNotConnectionsFolderException; import com.rapidminer.repository.RepositorySortingMethod; import com.rapidminer.repository.RepositorySortingMethodListener; +import com.rapidminer.repository.RepositoryStoreOtherInConnectionsFolderException; +import com.rapidminer.repository.RepositoryTools; import com.rapidminer.repository.gui.actions.AbstractRepositoryAction; import com.rapidminer.repository.gui.actions.ConfigureRepositoryAction; import com.rapidminer.repository.gui.actions.CopyEntryRepositoryAction; import com.rapidminer.repository.gui.actions.CopyLocationAction; +import com.rapidminer.repository.gui.actions.CreateConnectionAction; import com.rapidminer.repository.gui.actions.CreateFolderAction; import com.rapidminer.repository.gui.actions.CutEntryRepositoryAction; import com.rapidminer.repository.gui.actions.DeleteRepositoryEntryAction; +import com.rapidminer.repository.gui.actions.EditConnectionAction; import com.rapidminer.repository.gui.actions.OpenEntryAction; import com.rapidminer.repository.gui.actions.OpenInFileBrowserAction; import com.rapidminer.repository.gui.actions.PasteEntryRepositoryAction; @@ -107,6 +115,7 @@ import com.rapidminer.repository.gui.actions.SortByLastModifiedAction; import com.rapidminer.repository.gui.actions.SortByNameAction; import com.rapidminer.repository.gui.actions.StoreProcessAction; +import com.rapidminer.repository.internal.remote.RemoteRepository; import com.rapidminer.repository.local.LocalRepository; import com.rapidminer.studio.io.gui.internal.DataImportWizardBuilder; import com.rapidminer.studio.io.gui.internal.DataImportWizardUtils; @@ -250,12 +259,23 @@ public boolean importData(final TransferSupport ts) { * * Additionally, at first it is checked, if the operations are allowed. */ - private boolean copyOrMoveRepositoryEntries(final Entry droppedOnEntry, final List locations, + private boolean copyOrMoveRepositoryEntries(Entry droppedOnEntry, final List locations, final TransferSupport ts) throws RepositoryException { + final Folder droppedOnFolder; + if (droppedOnEntry instanceof Folder) { + droppedOnFolder = (Folder) droppedOnEntry; + } else if (isMoveOperation(ts, false)) { + // ignore move operations that don't target a folder + return false; + } else { + // use parent folder for copy operations + droppedOnFolder = droppedOnEntry.getContainingFolder(); + } + // First check, if operation is allowed for all locations for (RepositoryLocation location : locations) { - if (!copyOrMoveCheck(droppedOnEntry, location, ts)) { + if (!copyOrMoveCheck(droppedOnFolder, location, ts)) { return false; } } @@ -307,7 +327,7 @@ public void run() { isSingleEntryOperation = false; } - TreePath droppedOnPath = RepositoryTreeModel.getPathTo(droppedOnEntry, + TreePath droppedOnPath = RepositoryTreeModel.getPathTo(droppedOnFolder, RepositoryManager.getInstance(null)); // Initialize progress listener @@ -321,7 +341,7 @@ public void run() { try { // Entry already exists, overwrite? final String effectiveNewName = location.locateEntry().getName(); - if (((Folder) droppedOnEntry).containsEntry(effectiveNewName)) { + if (droppedOnFolder.containsEntry(effectiveNewName)) { // Do not confuse user with incorrect selected paths RepositoryTree.this.setSelectionPath(droppedOnPath); @@ -385,7 +405,7 @@ public Integer run() { // Do copy or move operation // Extracted to own method for lock handling and retry calls - boolean done = executeCopyOrMoveOperation(location, isRepositoryInLocations, isSingleEntryOperation, + boolean done = executeCopyOrMoveOperation(location, droppedOnFolder, isRepositoryInLocations, isSingleEntryOperation, userDecisions.overwriteIfExists); if (!done) { break; @@ -396,12 +416,12 @@ public Integer run() { // On multi-operations, select the target folder after finishing copy/move // operation - if (droppedOnEntry != null && !isSingleEntryOperation) { + if (droppedOnFolder != null && !isSingleEntryOperation) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { - TreePath droppedOnPath = RepositoryTreeModel.getPathTo(droppedOnEntry, + TreePath droppedOnPath = RepositoryTreeModel.getPathTo(droppedOnFolder, RepositoryManager.getInstance(null)); RepositoryTree.this.setSelectionPath(droppedOnPath); RepositoryTree.this.scrollPathToVisible(droppedOnPath); @@ -418,7 +438,7 @@ public void run() { * @return false if the user chose to cancel the operation; * true otherwise */ - private boolean executeCopyOrMoveOperation(RepositoryLocation location, boolean isRepositoryInLocations, + private boolean executeCopyOrMoveOperation(RepositoryLocation location, Folder target, boolean isRepositoryInLocations, boolean isSingleEntryOperation, boolean overwriteIfExists) { try { ProgressListener progressListener = null; @@ -426,63 +446,133 @@ private boolean executeCopyOrMoveOperation(RepositoryLocation location, boolean progressListenerCompleted + PROGRESS_LISTENER_SINGLE_STEP_SIZE); if (isMoveOperation(ts, isRepositoryInLocations)) { - RepositoryManager.getInstance(null).move(location, (Folder) droppedOnEntry, null, + RepositoryManager.getInstance(null).move(location, target, null, overwriteIfExists, progressListener); // On drag and drop move operation with overwrite, two delete operations // are performed. This results in a selection of a wrong tree element. // For this case, select the element, which is the target of the drop // operation. - if (overwriteIfExists && droppedOnEntry != null) { - SwingUtilities.invokeLater(() -> RepositoryTree.this.setSelectionPath(RepositoryTreeModel.getPathTo(droppedOnEntry, + if (overwriteIfExists && target != null) { + SwingUtilities.invokeLater(() -> RepositoryTree.this.setSelectionPath(RepositoryTreeModel.getPathTo(target, RepositoryManager.getInstance(null)))); } } else { - RepositoryManager.getInstance(null).copy(location, (Folder) droppedOnEntry, null, + RepositoryManager.getInstance(null).copy(location, target, null, overwriteIfExists, progressListener); } - } catch (RepositoryException e) { - if (e.getCause() != null && e.getCause() instanceof PasswordInputCanceledException) { - // no extra dialog if login dialog was canceled - return false; + } catch (RepositoryConnectionsFolderImmutableException | RepositoryStoreOtherInConnectionsFolderException | RepositoryConnectionsNotSupportedException e) { + // this happens when trying to modify/move/delete the connections folder or when trying to store other things inside a connection folder + String errorKey; + if (e instanceof RepositoryConnectionsFolderImmutableException) { + errorKey = "error_modify_connections_folder"; + } else if (e instanceof RepositoryStoreOtherInConnectionsFolderException) { + errorKey = "error_copy_other_to_connections_folder"; + } else { + errorKey = "error_connections_not_supported"; } - // Do not show "cancel" option, if is is an single entry operation - final int dialogMode = isSingleEntryOperation ? ConfirmDialog.YES_NO_OPTION : ConfirmDialog.YES_NO_CANCEL_OPTION; final String locationName = location.getName() == null ? "" : location.getName(); final AtomicInteger result = new AtomicInteger(); - SwingTools.invokeAndWait(() -> { - final ConfirmDialog dialog; - if (e.getMessage() != null && !e.getMessage().isEmpty()) { - dialog = new ConfirmDialog(ProgressThreadDialog.getInstance(), "error_in_copy_entry_with_cause", - dialogMode, false, locationName, e.getMessage()); - } else { - dialog = new ConfirmDialog(ProgressThreadDialog.getInstance(), "error_in_copy_entry", - dialogMode, false, locationName); + if (isSingleEntryOperation) { + SwingTools.invokeAndWait(() -> SwingTools.showVerySimpleErrorMessage(ProgressThreadDialog.getInstance(), errorKey, locationName)); + } else { + SwingTools.invokeAndWait(() -> { + final ConfirmDialog dialog = new ConfirmDialog(ProgressThreadDialog.getInstance(), errorKey, + ConfirmDialog.OK_CANCEL_OPTION, false, locationName); + dialog.setVisible(true); + result.set(dialog.getReturnOption()); + }); + if (result.get() == ConfirmDialog.CANCEL_OPTION) { + // user cancels whole copy operation. No need to log, as this is not an unexpected error state + return false; } - dialog.setVisible(true); - result.set(dialog.getReturnOption()); - }); - - if (result.get() == ConfirmDialog.YES_OPTION) { - return executeCopyOrMoveOperation(location, isRepositoryInLocations, isSingleEntryOperation, - overwriteIfExists); - - } else if (result.get() == ConfirmDialog.CANCEL_OPTION) { + } + } catch (RepositoryNotConnectionsFolderException e) { + // this happens when trying to move a connection into a non-connection special folder AND that repository knows about this + // we offer to automatically move them to the appropriate connections folder for that repository instead + final int dialogMode = isSingleEntryOperation ? ConfirmDialog.YES_NO_OPTION : ConfirmDialog.YES_NO_CANCEL_OPTION; + final String locationName = location.getName() == null ? "" : location.getName(); + final AtomicInteger result = new AtomicInteger(); + Folder repoConnectionsFolder = null; + try { + repoConnectionsFolder = RepositoryTools.getConnectionFolder(target.getLocation().getRepository()); + } catch (RepositoryException e1) { LogService.getRoot().log(Level.WARNING, - "com.rapidminer.repository.RepositoryTree.error_during_copying", e); - return false; + "com.rapidminer.repository.RepositoryTree.error_resolving_connections_folder", e1); + } + if (repoConnectionsFolder != null) { + String connLocRepo = repoConnectionsFolder.getLocation().getRepositoryName(); + SwingTools.invokeAndWait(() -> { + final ConfirmDialog dialog = new ConfirmDialog(ProgressThreadDialog.getInstance(), "error_copy_to_non_connections_folder", + dialogMode, false, e.getMessage(), locationName, connLocRepo); + dialog.setVisible(true); + result.set(dialog.getReturnOption()); + }); + + // user wants to automatically target the connection to the connections folder, then do it + // otherwise, we do nothing as logging is pointless for a non-error scenario + if (result.get() == ConfirmDialog.YES_OPTION) { + return executeCopyOrMoveOperation(location, repoConnectionsFolder, isRepositoryInLocations, isSingleEntryOperation, + overwriteIfExists); + } } else { - LogService.getRoot().log(Level.WARNING, - "com.rapidminer.repository.RepositoryTree.error_during_copying", e); - // No retry and no cancel, just go on + // if we for whatever reason cannot resolve the repository connections folder, we go to the regular error handling + return handleRepoException(e, location, target, isRepositoryInLocations, isSingleEntryOperation, overwriteIfExists); } + } catch (RepositoryException e) { + return handleRepoException(e, location, target, isRepositoryInLocations, isSingleEntryOperation, overwriteIfExists); } return true; } + + /** + * Handles a repository exception during copy/move. + * @return {@code true} if user pressed retry and it worked OR if he pressed no-retry; {@code false} if user pressed cancel + */ + private boolean handleRepoException(RepositoryException e, RepositoryLocation location, Folder target, boolean isRepositoryInLocations, boolean isSingleEntryOperation, boolean overwriteIfExists) { + if (e.getCause() != null && e.getCause() instanceof PasswordInputCanceledException) { + // no extra dialog if login dialog was canceled + return false; + } + // Do not show "cancel" option, if is is an single entry operation + final int dialogMode = isSingleEntryOperation ? ConfirmDialog.YES_NO_OPTION : ConfirmDialog.YES_NO_CANCEL_OPTION; + final String locationName = location.getName() == null ? "" : location.getName(); + final AtomicInteger result = new AtomicInteger(); + + SwingTools.invokeAndWait(() -> { + final ConfirmDialog dialog; + if (e.getMessage() != null && !e.getMessage().isEmpty()) { + dialog = new ConfirmDialog(ProgressThreadDialog.getInstance(), "error_in_copy_entry_with_cause", + dialogMode, false, locationName, e.getMessage()); + } else { + dialog = new ConfirmDialog(ProgressThreadDialog.getInstance(), "error_in_copy_entry", + dialogMode, false, locationName); + } + dialog.setVisible(true); + result.set(dialog.getReturnOption()); + }); + + if (result.get() == ConfirmDialog.YES_OPTION) { + return executeCopyOrMoveOperation(location, target, isRepositoryInLocations, isSingleEntryOperation, + overwriteIfExists); + + } else if (result.get() == ConfirmDialog.CANCEL_OPTION) { + LogService.getRoot().log(Level.WARNING, + "com.rapidminer.repository.RepositoryTree.error_during_copying", e); + return false; + + } else { + // user pressed "no-retry", so log and return true + LogService.getRoot().log(Level.WARNING, + "com.rapidminer.repository.RepositoryTree.error_during_copying", e); + return true; + } + } + }.start(); // No failures in initial check @@ -515,7 +605,7 @@ private boolean copyOrMoveCheck(final Entry droppedOnEntry, final RepositoryLoca } else { // Check for unknown parameters - RepositoryLocation targetLocation = ((Folder) droppedOnEntry).getLocation(); + RepositoryLocation targetLocation = droppedOnEntry.getLocation(); if (targetLocation == null) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.repository.RepositoryTree.parameter_missing.target_location"); @@ -539,6 +629,10 @@ private boolean copyOrMoveCheck(final Entry droppedOnEntry, final RepositoryLoca "com.rapidminer.repository.RepositoryTree.parameter_missing.repository_location"); return false; } + if (locationEntry instanceof Repository) { + SwingTools.showVerySimpleErrorMessage("repository_copy_repository"); + return false; + } String effectiveNewName = locationEntry.getName(); if (effectiveNewName == null || effectiveNewName.isEmpty()) { LogService.getRoot().log(Level.WARNING, @@ -722,32 +816,34 @@ public Class> getRepositoryActionClass() { private final int TREE_ROW_HEIGHT = 24; static { - addRepositoryAction(ConfigureRepositoryAction.class, new RepositoryActionConditionImplConfigRepository(), false, - true); + addRepositoryAction(ConfigureRepositoryAction.class, new RepositoryActionConditionImplConfigRepository(), false, true); addRepositoryAction(OpenEntryAction.class, - new RepositoryActionConditionImplStandard(new Class[] { DataEntry.class }, new Class[] {}), false, - false); - addRepositoryAction(StoreProcessAction.class, new RepositoryActionConditionImplStandard( - new Class[] { ProcessEntry.class, Folder.class }, new Class[] {}), false, false); + new RepositoryActionConditionImplStandard(new Class[]{DataEntry.class}), false, false); + addRepositoryAction(StoreProcessAction.class, + new RepositoryActionConditionAdditionallyNotConnections(new Class[]{ProcessEntry.class, Folder.class}, + true, true), false, false); addRepositoryAction(RenameRepositoryEntryAction.class, - new RepositoryActionConditionImplStandardNoRepository(new Class[] { Entry.class }, new Class[] {}), - false, false); + new RepositoryActionConditionAdditionallyNotConnections(new Class[]{Entry.class}, false), false, false); + addRepositoryAction(EditConnectionAction.class, new RepositoryActionConditionImplStandard(new Class[]{ConnectionEntry.class}, new Class[]{LocalRepository.class, RemoteRepository.class}), false, + false); + addRepositoryAction(CreateConnectionAction.class, new RepositoryActionConditionRepositoryAndConnections(new Class[]{Folder.class}), false, + false); addRepositoryAction(CreateFolderAction.class, - new RepositoryActionConditionImplStandard(new Class[] { Folder.class }, new Class[] {}), false, false); + new RepositoryActionConditionAdditionallyNotConnections(new Class[]{Folder.class}, true, true), false, false); addRepositoryAction(CutEntryRepositoryAction.class, - new RepositoryActionConditionImplStandardNoRepository(new Class[] {}, new Class[] {}), true, false); + new RepositoryActionConditionAdditionallyNotConnections(new Class[0], false), true, false); addRepositoryAction(CopyEntryRepositoryAction.class, - new RepositoryActionConditionImplStandard(new Class[] {}, new Class[] {}), false, false); + new RepositoryActionConditionAdditionallyNotConnections(new Class[0], false), false, false); addRepositoryAction(PasteEntryRepositoryAction.class, - new RepositoryActionConditionImplStandard(new Class[] {}, new Class[] {}), false, false); + new RepositoryActionConditionImplStandard(new Class[0]), false, false); addRepositoryAction(CopyLocationAction.class, - new RepositoryActionConditionImplStandard(new Class[] {}, new Class[] {}), false, false); + new RepositoryActionConditionImplStandard(new Class[0]), false, false); addRepositoryAction(DeleteRepositoryEntryAction.class, - new RepositoryActionConditionImplStandard(new Class[] { Entry.class }, new Class[] {}), false, false); + new RepositoryActionConditionAdditionallyNotConnections(new Class[]{Entry.class}, true), false, false); addRepositoryAction(RefreshRepositoryEntryAction.class, - new RepositoryActionConditionImplStandard(new Class[] { Entry.class }, new Class[] {}), true, false); + new RepositoryActionConditionImplStandard(new Class[]{Entry.class}), true, false); addRepositoryAction(OpenInFileBrowserAction.class, new RepositoryActionConditionImplStandard( - new Class[] { Entry.class }, new Class[] { LocalRepository.class }), false, false); + new Class[]{Entry.class}, new Class[]{LocalRepository.class}), false, false); } public RepositoryTree() { @@ -951,13 +1047,7 @@ public void keyPressed(KeyEvent e) { setTransferHandler(new RepositoryTreeTransferhandler()); } - getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() { - - @Override - public void valueChanged(TreeSelectionEvent e) { - enableActions(); - } - }); + getSelectionModel().addTreeSelectionListener(e -> enableActions()); addTreeExpansionListener(new TreeExpansionListener() { @@ -1119,6 +1209,9 @@ boolean expandIfExists(RepositoryLocation relativeTo, String location) { full = false; } if (entry != null) { + if (relativeTo != null) { + RepositoryManager.getInstance(null).unhide(relativeTo.getRepositoryName()); + } RepositoryTreeModel model = (RepositoryTreeModel) getModel(); TreePath pathTo = model.getPathTo(entry); expandPath(pathTo); @@ -1207,7 +1300,7 @@ private void showPopup(MouseEvent e) { try { Constructor> constructor = actionEntry.getRepositoryActionClass() - .getConstructor(new Class[] { RepositoryTree.class }); + .getConstructor(RepositoryTree.class); AbstractRepositoryAction createdAction = constructor.newInstance(this); createdAction.enable(); @@ -1289,7 +1382,7 @@ public List getSelectedEntries() { public Collection> getAllActions() { List> listOfAbstractRepositoryActions = new LinkedList<>(); - for (Action action : createContextMenuActions(this, new LinkedList())) { + for (Action action : createContextMenuActions(this, new LinkedList<>())) { if (action instanceof AbstractRepositoryAction) { listOfAbstractRepositoryActions.add((AbstractRepositoryAction) action); } @@ -1313,7 +1406,6 @@ public Collection> getAllActions() { * if true, a separator will be added before the action * @param hasSeparatorAfter * if true, a separator will be added after the action - * @return true if the action was successfully added; false otherwise */ public static void addRepositoryAction(Class> actionClass, RepositoryActionCondition condition, boolean hasSeparatorBefore, boolean hasSeparatorAfter) { @@ -1340,7 +1432,6 @@ public static void addRepositoryAction(Class> actionClass, RepositoryActionCondition condition, Class insertAfterThisAction, boolean hasSeparatorBefore, @@ -1386,13 +1477,7 @@ public static void addRepositoryAction(Class> actionClass) { - Iterator iterator = REPOSITORY_ACTIONS.iterator(); - - while (iterator.hasNext()) { - if (iterator.next().getRepositoryActionClass().equals(actionClass)) { - iterator.remove(); - } - } + REPOSITORY_ACTIONS.removeIf(repositoryActionEntry -> repositoryActionEntry.getRepositoryActionClass().equals(actionClass)); } /** @@ -1415,7 +1500,7 @@ private static List createContextMenuActions(RepositoryTree repositoryTr listOfActions.add(null); } Constructor> constructor = actionEntry.getRepositoryActionClass() - .getConstructor(new Class[] { RepositoryTree.class }); + .getConstructor(RepositoryTree.class); AbstractRepositoryAction createdAction = constructor.newInstance(repositoryTree); createdAction.enable(); listOfActions.add(createdAction); diff --git a/src/main/java/com/rapidminer/repository/gui/RepositoryTreeCellRenderer.java b/src/main/java/com/rapidminer/repository/gui/RepositoryTreeCellRenderer.java index 78281c386..121e3ffeb 100644 --- a/src/main/java/com/rapidminer/repository/gui/RepositoryTreeCellRenderer.java +++ b/src/main/java/com/rapidminer/repository/gui/RepositoryTreeCellRenderer.java @@ -1,25 +1,26 @@ /** * Copyright (C) 2001-2019 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.repository.gui; import java.awt.Component; import java.text.DateFormat; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -30,9 +31,12 @@ import javax.swing.border.Border; import javax.swing.tree.DefaultTreeCellRenderer; +import com.rapidminer.connection.util.ConnectionI18N; import com.rapidminer.gui.renderer.RendererService; +import com.rapidminer.gui.tools.IconSize; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.repository.BlobEntry; +import com.rapidminer.repository.ConnectionEntry; import com.rapidminer.repository.DataEntry; import com.rapidminer.repository.Entry; import com.rapidminer.repository.Folder; @@ -54,15 +58,17 @@ public class RepositoryTreeCellRenderer extends DefaultTreeCellRenderer { private static final Icon ICON_FOLDER_CLOSED = SwingTools.createIcon("16/folder.png"); private static final Icon ICON_FOLDER_OPEN_LOCKED = SwingTools.createIcon("16/folder_open_lock.png"); private static final Icon ICON_FOLDER_CLOSED_LOCKED = SwingTools.createIcon("16/folder_lock.png"); + private static final Icon ICON_CONNECTION_FOLDER = SwingTools.createIcon("16/plug.png"); private static final Icon ICON_PROCESS = SwingTools.createIcon("16/gearwheels.png"); private static final Icon ICON_DATA = SwingTools.createIcon("16/data.png"); private static final Icon ICON_BLOB = SwingTools.createIcon("16/document_empty.png"); private static final Icon ICON_TEXT = SwingTools.createIcon("16/text.png"); private static final Icon ICON_TABLE = SwingTools.createIcon("16/spreadsheet.png"); private static final Icon ICON_IMAGE = SwingTools.createIcon("16/photo_landscape.png"); + private static final Icon ICON_CONNECTION_INFORMATION = SwingTools.createIcon("16/" + ConnectionI18N.CONNECTION_ICON); /** stores the icons for all repository implementations */ - private static Map ICON_REPOSITORY_MAP = new HashMap<>(); + private static Map ICON_REPOSITORY_MAP = Collections.synchronizedMap(new HashMap<>()); // clone because getDateInstance uses an internal pool which can return the same // instance for multiple threads @@ -73,7 +79,7 @@ public class RepositoryTreeCellRenderer extends DefaultTreeCellRenderer { @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, - int row, boolean hasFocus) { + int row, boolean hasFocus) { JLabel label = (JLabel) super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); if (value instanceof Entry) { Entry entry = (Entry) value; @@ -131,7 +137,9 @@ public Component getTreeCellRendererComponent(JTree tree, Object value, boolean } label.setIcon(ICON_REPOSITORY_MAP.get(repo.getIconName())); } else if (entry.getType().equals(Folder.TYPE_NAME)) { - if (entry.isReadOnly() && !expanded) { + if (((Folder) entry).isSpecialConnectionsFolder()) { + label.setIcon(ICON_CONNECTION_FOLDER); + }else if (entry.isReadOnly() && !expanded) { label.setIcon(ICON_FOLDER_CLOSED_LOCKED); } else if (entry.isReadOnly() && expanded) { label.setIcon(ICON_FOLDER_OPEN_LOCKED); @@ -165,6 +173,8 @@ public Component getTreeCellRendererComponent(JTree tree, Object value, boolean } else { label.setIcon(ICON_BLOB); } + } else if (entry.getType().equals(ConnectionEntry.TYPE_NAME)) { + label.setIcon(getConnectionIcon(entry)); } else { label.setIcon(null); } @@ -174,13 +184,40 @@ public Component getTreeCellRendererComponent(JTree tree, Object value, boolean return label; } + /** + * Retrieve a cached icon for the given icon name for a repository. + * + * @param iconName + * name of the icon as set in {@link Repository#getIconName()} + * @return the icon, can be null + * @since 9.3 + */ + public static Icon getRepositoryIcon(String iconName) { + return ICON_REPOSITORY_MAP.get(iconName); + } + + /** + * Returns the connection icon for the entry + * + * @param entry the entry + * @return the icon for the entry or {@link #ICON_CONNECTION_INFORMATION} + */ + private static Icon getConnectionIcon(Entry entry) { + try { + return ConnectionI18N.getConnectionIcon(((ConnectionEntry) entry).getConnectionType(), IconSize.SMALL); + } catch (Exception e) { + // don't care, just show the default icon + return ICON_CONNECTION_INFORMATION; + } + } + /** * Appends a - to the provided StringBuilder * * @param state * the StringBuilder to add the - to */ - private void appendDash(StringBuilder state) { + private static void appendDash(StringBuilder state) { state.append(" – "); } } diff --git a/src/main/java/com/rapidminer/repository/gui/RepositoryTreeModel.java b/src/main/java/com/rapidminer/repository/gui/RepositoryTreeModel.java index 955d8d380..7d39fd434 100644 --- a/src/main/java/com/rapidminer/repository/gui/RepositoryTreeModel.java +++ b/src/main/java/com/rapidminer/repository/gui/RepositoryTreeModel.java @@ -79,7 +79,7 @@ private TreeModelEvent makeChangeEvent(Entry entry) { TreePath path = getPathTo(entry.getContainingFolder()); int index; if (entry instanceof Repository) { - index = RepositoryManager.getInstance(null).getRepositories().indexOf(entry); + index = RepositoryManager.getInstance(null).getFilteredRepositories().indexOf(entry); } else { index = getIndexOfChild(entry.getContainingFolder(), entry); } @@ -279,7 +279,7 @@ public Object getChild(Object parent, int index) { if (onlyWriteableRepositories) { return getWritableRepositories((RepositoryManager) parent).get(index); } - return ((RepositoryManager) parent).getRepositories().get(index); + return ((RepositoryManager) parent).getFilteredRepositories().get(index); } else if (parent instanceof Folder) { Folder folder = (Folder) parent; if (folder.willBlock()) { @@ -409,7 +409,7 @@ public int getChildCount(Object parent) { if (onlyWriteableRepositories) { return getWritableRepositories((RepositoryManager) parent).size(); } - return ((RepositoryManager) parent).getRepositories().size(); + return ((RepositoryManager) parent).getFilteredRepositories().size(); } else if (parent instanceof Folder) { Folder folder = (Folder) parent; if (folder.willBlock()) { @@ -456,7 +456,7 @@ public int getIndexOfChild(Object parent, Object child) { if (onlyWriteableRepositories) { return getWritableRepositories((RepositoryManager) parent).indexOf(child); } - return ((RepositoryManager) parent).getRepositories().indexOf(child); + return ((RepositoryManager) parent).getFilteredRepositories().indexOf(child); } else if (parent instanceof Folder) { // don't return -1 for index of pending "folder" (for blocking folder requests) if (PENDING_FOLDER_NAME.equals(child)) { @@ -534,7 +534,7 @@ public void valueForPathChanged(TreePath path, Object newValue) { } private List getWritableRepositories(RepositoryManager manager) { - List repositories = manager.getRepositories(); + List repositories = manager.getFilteredRepositories(); List writeableRepositories = new ArrayList<>(); for (Repository repository : repositories) { if (!repository.isReadOnly()) { @@ -562,13 +562,22 @@ void setSortingMethod(RepositorySortingMethod method) { treeUtil.saveExpansionState(parentTree); } - TreeModelEvent e = new TreeModelEvent(RepositoryTreeModel.this, getPathTo(null), new int[] { 0 }, - new Object[] { null }); + notifyTreeStructureChanged(); + + SwingUtilities.invokeLater(() -> treeUtil.restoreSelectionPaths(parentTree)); + } + + /** + * Changes from {@link com.rapidminer.repository.RepositoryFilter} may need to trigger an update here + * + * @since 9.3 + */ + public void notifyTreeStructureChanged() { + TreeModelEvent e = new TreeModelEvent(RepositoryTreeModel.this, getPathTo(null), new int[]{0}, + new Object[]{null}); for (TreeModelListener l : listeners.getListeners(TreeModelListener.class)) { l.treeStructureChanged(e); } - - SwingUtilities.invokeLater(() -> treeUtil.restoreSelectionPaths(parentTree)); } /** diff --git a/src/main/java/com/rapidminer/repository/gui/actions/AbstractRepositoryAction.java b/src/main/java/com/rapidminer/repository/gui/actions/AbstractRepositoryAction.java index 34bfec788..ba3643a45 100644 --- a/src/main/java/com/rapidminer/repository/gui/actions/AbstractRepositoryAction.java +++ b/src/main/java/com/rapidminer/repository/gui/actions/AbstractRepositoryAction.java @@ -29,7 +29,10 @@ import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.gui.tools.dialogs.ConfirmDialog; import com.rapidminer.repository.Entry; +import com.rapidminer.repository.Folder; +import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; +import com.rapidminer.repository.RepositoryTools; import com.rapidminer.repository.gui.RepositoryTree; @@ -105,6 +108,14 @@ public void loggedActionPerformed(ActionEvent e) { if (e.getActionCommand().equals(DeleteRepositoryEntryAction.I18N_KEY) || e.getActionCommand().equals(CutCopyPasteDeleteAction.DELETE_ACTION_COMMAND_KEY)) { if (entries.size() == 1) { + try { + Folder connectionFolder = RepositoryTools.getConnectionFolder(entries.get(0).getLocation().getRepository()); + if (connectionFolder != null && connectionFolder.getLocation().getAbsoluteLocation().equals(entries.get(0).getLocation().getAbsoluteLocation())) { + return; + } + } catch (RepositoryException e1) { + // ignore, should not happen anyway and is irrelevant here. Worst case, you get the delete dialog if you should not, but backend blocks delete anyway + } if (SwingTools.showConfirmDialog("file_chooser.delete", ConfirmDialog.YES_NO_OPTION, entries.get(0).getName()) != ConfirmDialog.YES_OPTION) { return; diff --git a/src/main/java/com/rapidminer/repository/gui/actions/CreateConnectionAction.java b/src/main/java/com/rapidminer/repository/gui/actions/CreateConnectionAction.java new file mode 100644 index 000000000..66fc9c99d --- /dev/null +++ b/src/main/java/com/rapidminer/repository/gui/actions/CreateConnectionAction.java @@ -0,0 +1,66 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.repository.gui.actions; + +import com.rapidminer.connection.gui.ConnectionCreationDialog; +import com.rapidminer.gui.ApplicationFrame; +import com.rapidminer.repository.Folder; +import com.rapidminer.repository.Repository; +import com.rapidminer.repository.RepositoryException; +import com.rapidminer.repository.gui.RepositoryTree; + + +/** + * Action to create a new {@link com.rapidminer.connection.ConnectionInformation Connection}. + * + * @author Marco Boeck + * @since 9.3.0 + */ +public class CreateConnectionAction extends AbstractRepositoryAction { + + public CreateConnectionAction(RepositoryTree tree) { + super(tree, Folder.class, true, "repository_create_connection"); + } + + + @Override + public void actionPerformed(Folder folder) { + createConnection(folder); + } + + /** + * Opens the connection creation dialog. If a folder is given and that repository does not support connections, will + * act as if no predefined location was passed at all. + * + * @param folder + * the optional predefined folder/repository where the new connection should be created. Can be {@code null} + */ + public static void createConnection(Folder folder) { + Repository repo = null; + try { + repo = folder != null ? folder.getLocation().getRepository() : null; + if (repo != null && !repo.supportsConnections()) { + repo = null; + } + } catch (RepositoryException e) { + // ignore, should not happen anyway + } + new ConnectionCreationDialog(ApplicationFrame.getApplicationFrame(), repo).setVisible(true); + } +} diff --git a/src/main/java/com/rapidminer/repository/gui/actions/EditConnectionAction.java b/src/main/java/com/rapidminer/repository/gui/actions/EditConnectionAction.java new file mode 100644 index 000000000..38d967b7a --- /dev/null +++ b/src/main/java/com/rapidminer/repository/gui/actions/EditConnectionAction.java @@ -0,0 +1,131 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.repository.gui.actions; + +import java.io.IOException; + +import javax.swing.SwingUtilities; + +import com.rapidminer.connection.gui.ConnectionEditDialog; +import com.rapidminer.connection.gui.actions.SaveConnectionAction; +import com.rapidminer.connection.gui.dto.ConnectionInformationHolder; +import com.rapidminer.gui.tools.ProgressThread; +import com.rapidminer.gui.tools.ProgressThreadDialog; +import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.repository.ConnectionEntry; +import com.rapidminer.repository.RepositoryException; +import com.rapidminer.repository.gui.RepositoryTree; +import com.rapidminer.repository.internal.remote.RemoteConnectionEntry; +import com.rapidminer.tools.PasswordInputCanceledException; + + +/** + * Edit Connection Action + * + * @author Jonas Wilms-Pfau + * @since 9.3.0 + */ +public class EditConnectionAction extends AbstractRepositoryAction { + + private static final String PROGRESS_THREAD_KEY = "download_connection_from_repository"; + + public EditConnectionAction(RepositoryTree tree) { + super(tree, ConnectionEntry.class, true, "repository_edit_connection"); + } + + @Override + public void actionPerformed(ConnectionEntry entry) { + editConnection(entry, true); + } + + /** + * Opens the connection edit dialog. Note that the retrieval happens async, so this may take a while before the + * dialog will appear. A {@link com.rapidminer.gui.tools.ProgressThread} will be visible while the retrieval is in + * progress. + * + * @param connection + * the connection entry + * @param openInEditMode + * {@code true} if the connection should be opened in edit mode + */ + public static void editConnection(ConnectionEntry connection, boolean openInEditMode) { + if (containsId(ProgressThread.getQueuedThreads(), PROGRESS_THREAD_KEY) || + containsId(ProgressThread.getCurrentThreads(), PROGRESS_THREAD_KEY)) { + // Must happen after the frequent dispose calls in ProgressThreadDialog#updateUI + SwingUtilities.invokeLater(() -> ProgressThreadDialog.getInstance().setVisible(true)); + return; + } + + final ProgressThread downloadProgressThread = new ProgressThread(PROGRESS_THREAD_KEY, false, connection.getLocation().toString()) { + + @Override + public void run() { + try { + ConnectionInformationHolder ci = ConnectionInformationHolder.from(connection); + SwingTools.invokeLater(() -> new ConnectionEditDialog(ci, openInEditMode).setVisible(true)); + } catch (RepositoryException e) { + if (connection instanceof RemoteConnectionEntry) { + SwingTools.showSimpleErrorMessage("error_contacting_repository", e, e.getMessage()); + } else { + SwingTools.showSimpleErrorMessage("connection_read_error", e, e.getMessage()); + } + } catch (IOException e) { + SwingTools.showSimpleErrorMessage("connection_read_error", e, e.getMessage()); + } catch (PasswordInputCanceledException e) { + SwingTools.showSimpleErrorMessage("error_access_rights", e, e.getMessage()); + } + } + + @Override + public String getID() { + return super.getID() + connection.getLocation(); + } + }; + downloadProgressThread.setStartDialogShowTimer(true); + downloadProgressThread.setShowDialogTimerDelay(1500); + downloadProgressThread.setIndeterminate(true); + downloadProgressThread.addDependency(downloadProgressThread.getID()); + String saveConnectionID = SaveConnectionAction.PROGRESS_THREAD_ID_PREFIX + connection.getLocation(); + downloadProgressThread.addDependency(saveConnectionID); + downloadProgressThread.start(); + // Show progress thread dialog to indicate that editing is blocked by saving + if (containsId(ProgressThread.getQueuedThreads(), saveConnectionID) || + containsId(ProgressThread.getCurrentThreads(), saveConnectionID)) { + // Must happen after the frequent dispose calls in ProgressThreadDialog#updateUI + SwingUtilities.invokeLater(() -> ProgressThreadDialog.getInstance().setVisible(true)); + } + } + + /** + * Checks if the given list contains a progress thread with the id + * + * @param progressThreads + * a list of progress threads + * @param id the {@link ProgressThread#getID()} + * @return {@code true} if the list contains a progress thread with the id {@value #PROGRESS_THREAD_KEY} + */ + private static boolean containsId(Iterable progressThreads, String id) { + for (ProgressThread pt : progressThreads) { + if (id.equals(pt.getID())) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/rapidminer/repository/gui/actions/OpenEntryAction.java b/src/main/java/com/rapidminer/repository/gui/actions/OpenEntryAction.java index 40edcf1fb..9561f3463 100644 --- a/src/main/java/com/rapidminer/repository/gui/actions/OpenEntryAction.java +++ b/src/main/java/com/rapidminer/repository/gui/actions/OpenEntryAction.java @@ -19,6 +19,7 @@ package com.rapidminer.repository.gui.actions; import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.repository.ConnectionEntry; import com.rapidminer.repository.DataEntry; import com.rapidminer.repository.IOObjectEntry; import com.rapidminer.repository.ProcessEntry; @@ -40,7 +41,9 @@ public OpenEntryAction(RepositoryTree tree) { @Override public void actionPerformed(DataEntry data) { - if (data instanceof IOObjectEntry) { + if (data instanceof ConnectionEntry){ + com.rapidminer.gui.actions.OpenAction.showConnectionInformationDialog((ConnectionEntry) data); + } else if (data instanceof IOObjectEntry) { com.rapidminer.gui.actions.OpenAction.showAsResult((IOObjectEntry) data); } else if (data instanceof ProcessEntry) { RepositoryTree.openProcess((ProcessEntry) data); diff --git a/src/main/java/com/rapidminer/repository/gui/actions/RenameRepositoryEntryAction.java b/src/main/java/com/rapidminer/repository/gui/actions/RenameRepositoryEntryAction.java index 0e3a7d1ac..d8fef00ad 100644 --- a/src/main/java/com/rapidminer/repository/gui/actions/RenameRepositoryEntryAction.java +++ b/src/main/java/com/rapidminer/repository/gui/actions/RenameRepositoryEntryAction.java @@ -21,6 +21,7 @@ import com.rapidminer.gui.tools.ProgressThread; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.repository.Entry; +import com.rapidminer.repository.Folder; import com.rapidminer.repository.Repository; import com.rapidminer.repository.gui.RepositoryTree; @@ -40,10 +41,17 @@ public RenameRepositoryEntryAction(RepositoryTree tree) { @Override public void actionPerformed(Entry entry) { - // no renaming of repositores allowed, RepositoryConfigurationDialog is responsible for that + // no renaming of repositories allowed, RepositoryConfigurationDialog is responsible for that if (entry instanceof Repository) { return; } + if (entry instanceof Folder) { + Folder f = (Folder) entry; + // if this is the connections folder AND it is named properly as "Connections" + if (f.isSpecialConnectionsFolder() && Folder.isConnectionsFolderName(f.getName(), true)) { + return; + } + } String name = SwingTools.showRepositoryEntryInputDialog("file_chooser.rename", entry.getName(), entry.getName()); if ((name != null) && !name.equals(entry.getName())) { diff --git a/src/main/java/com/rapidminer/repository/gui/actions/ShowProcessInRepositoryAction.java b/src/main/java/com/rapidminer/repository/gui/actions/ShowProcessInRepositoryAction.java index d5cdb3958..5bc1d34db 100644 --- a/src/main/java/com/rapidminer/repository/gui/actions/ShowProcessInRepositoryAction.java +++ b/src/main/java/com/rapidminer/repository/gui/actions/ShowProcessInRepositoryAction.java @@ -49,6 +49,9 @@ public void loggedActionPerformed(ActionEvent e) { if (RapidMinerGUI.getMainFrame().getProcess() != null) { RepositoryLocation repoLoc = RapidMinerGUI.getMainFrame().getProcess().getRepositoryLocation(); if (repoLoc != null) { + // scroll to location + // twice because otherwise the repository browser selects the parent... + tree.expandAndSelectIfExists(repoLoc); tree.expandAndSelectIfExists(repoLoc); } } diff --git a/src/main/java/com/rapidminer/repository/gui/actions/SortByAction.java b/src/main/java/com/rapidminer/repository/gui/actions/SortByAction.java index 876ae6f75..81bbe442d 100644 --- a/src/main/java/com/rapidminer/repository/gui/actions/SortByAction.java +++ b/src/main/java/com/rapidminer/repository/gui/actions/SortByAction.java @@ -38,31 +38,31 @@ public abstract class SortByAction extends ToggleAction { private static final long serialVersionUID = 1L; - private RepositoryTree tree; - - private RepositorySortingMethod method; + private final RepositoryTree tree; + private final RepositorySortingMethod method; + /** + * Creates a new SortByAction + * + * @param i18n the i18n key + * @param tree the repository tree on which the sorting method is applied + * @param method the sorting method represented by this action + */ public SortByAction(String i18n, RepositoryTree tree, RepositorySortingMethod method) { super(true, i18n); this.tree = tree; this.method = method; - ToggleAction thisAction = this; - if (tree.getSortingMethod() != method) { - this.setSelected(false); - } else { - this.setSelected(true); - } - tree.addRepostorySortingMethodListener(new RepositorySortingMethodListener() { + tree.addRepostorySortingMethodListener(this::updateSelectedStatus); + updateSelectedStatus(tree.getSortingMethod()); + } - @Override - public void changedRepositorySortingMethod(RepositorySortingMethod changedToMethod) { - if (changedToMethod != method) { - thisAction.setSelected(false); - } else { - thisAction.setSelected(true); - } - } - }); + /** + * Updates the selected status + * + * @param selectedMethod the currently selected method + */ + private void updateSelectedStatus(RepositorySortingMethod selectedMethod){ + setSelected(selectedMethod == method); } @Override diff --git a/src/main/java/com/rapidminer/repository/gui/search/RepositoryGlobalSearchGUIProvider.java b/src/main/java/com/rapidminer/repository/gui/search/RepositoryGlobalSearchGUIProvider.java index ae6b1c90f..1a45a800c 100644 --- a/src/main/java/com/rapidminer/repository/gui/search/RepositoryGlobalSearchGUIProvider.java +++ b/src/main/java/com/rapidminer/repository/gui/search/RepositoryGlobalSearchGUIProvider.java @@ -36,11 +36,11 @@ import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; -import javax.swing.SwingWorker; import javax.swing.border.Border; import org.apache.lucene.document.Document; +import com.rapidminer.connection.util.ConnectionI18N; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.actions.OpenAction; import com.rapidminer.gui.dnd.TransferableRepositoryEntry; @@ -48,8 +48,10 @@ import com.rapidminer.gui.search.GlobalSearchGUIUtilities; import com.rapidminer.gui.search.GlobalSearchableGUIProvider; import com.rapidminer.gui.tools.IconSize; +import com.rapidminer.gui.tools.MultiSwingWorker; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.operator.Model; +import com.rapidminer.repository.ConnectionEntry; import com.rapidminer.repository.ConnectionRepository; import com.rapidminer.repository.Entry; import com.rapidminer.repository.IOObjectEntry; @@ -257,7 +259,7 @@ public DragGestureListener getDragAndDropSupport(final Document document) { * the type icon label */ private void loadEntryDetailsAsync(final Document document, final JLabel iconListLabel) { - SwingWorker worker = new SwingWorker() { + MultiSwingWorker worker = new MultiSwingWorker() { @Override protected Entry doInBackground() throws Exception { @@ -278,7 +280,7 @@ protected void done() { // no error here? Entry located successfully. // put icon in cache for faster retrieval next time - Icon icon = getIconForEntry(locatedEntry); + Icon icon = getIconForEntry(locatedEntry, document); ICON_CACHE.put(locatedEntry.getLocation().getAbsoluteLocation(), icon); iconListLabel.setIcon(icon); @@ -290,7 +292,7 @@ protected void done() { } } }; - worker.execute(); + worker.start(); } /** @@ -300,7 +302,7 @@ protected void done() { * the document for which to load the repository entry */ private void openEntryAsync(final Document document) { - SwingWorker worker = new SwingWorker() { + MultiSwingWorker worker = new MultiSwingWorker() { @Override protected Entry doInBackground() throws Exception { @@ -316,6 +318,8 @@ protected void done() { if (locatedEntry instanceof ProcessEntry) { RepositoryTree.openProcess((ProcessEntry) locatedEntry); + } else if (locatedEntry instanceof ConnectionEntry) { + OpenAction.showConnectionInformationDialog((ConnectionEntry) locatedEntry); } else if (locatedEntry instanceof IOObjectEntry) { OpenAction.showAsResult((IOObjectEntry) locatedEntry); } else { @@ -328,7 +332,7 @@ protected void done() { } } }; - worker.execute(); + worker.start(); } /** @@ -353,13 +357,19 @@ private static String formatName(final String name, final String[] bestFragments * * @param entry * the entry, must not be {@code null} + * @param document + * the document, must not be {@code null} * @return the icon, never {@code null} */ - private static Icon getIconForEntry(final Entry entry) { + private static Icon getIconForEntry(final Entry entry, final Document document) { if (entry instanceof ProcessEntry) { return PROCESS_ICON; } else if (entry instanceof Model) { return MODEL_ICON; + } else if (entry instanceof ConnectionEntry) { + String connectionType = document.get(RepositoryGlobalSearch.FIELD_CONNECTION_TYPE); + // server connections do not have the type known unless detailed indexing is enabled, so make sure we show the generic connection icon and not crash + return ConnectionI18N.getConnectionIcon(connectionType != null ? connectionType : "", IconSize.SMALL); } else if (entry instanceof IOObjectEntry) { IOObjectEntry dataEntry = (IOObjectEntry) entry; return RendererService.getIcon(dataEntry.getObjectClass(), IconSize.SMALL); diff --git a/src/main/java/com/rapidminer/repository/internal/remote/RESTRepository.java b/src/main/java/com/rapidminer/repository/internal/remote/RESTRepository.java index bfa3eb0bd..f00d734a6 100644 --- a/src/main/java/com/rapidminer/repository/internal/remote/RESTRepository.java +++ b/src/main/java/com/rapidminer/repository/internal/remote/RESTRepository.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ package com.rapidminer.repository.internal.remote; import java.io.IOException; diff --git a/src/main/java/com/rapidminer/repository/internal/remote/RemoteConnectionEntry.java b/src/main/java/com/rapidminer/repository/internal/remote/RemoteConnectionEntry.java new file mode 100644 index 000000000..08d877dfd --- /dev/null +++ b/src/main/java/com/rapidminer/repository/internal/remote/RemoteConnectionEntry.java @@ -0,0 +1,31 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.repository.internal.remote; + +import com.rapidminer.repository.ConnectionEntry; + +/** + * Representation for remote {@link ConnectionEntry}s. It combines the interfaces + * {@link ConnectionEntry} and {@link RemoteIOObjectEntry}. + * + * @author Andreas Timm + * @since 9.3 + */ +public interface RemoteConnectionEntry extends RemoteIOObjectEntry, ConnectionEntry { +} diff --git a/src/main/java/com/rapidminer/repository/internal/remote/RemoteContentManager.java b/src/main/java/com/rapidminer/repository/internal/remote/RemoteContentManager.java index 07b6c65c4..95ca3f7bd 100644 --- a/src/main/java/com/rapidminer/repository/internal/remote/RemoteContentManager.java +++ b/src/main/java/com/rapidminer/repository/internal/remote/RemoteContentManager.java @@ -1,25 +1,24 @@ /** * Copyright (C) 2001-2019 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.repository.internal.remote; import java.util.List; - import javax.xml.datatype.XMLGregorianCalendar; import javax.xml.ws.BindingProvider; @@ -38,7 +37,6 @@ * * @author Nils Woehler * @since 6.5.0 - * */ public interface RemoteContentManager { @@ -46,12 +44,12 @@ public interface RemoteContentManager { * Retrieves remote entry information from the server. * * @param path - * the path to lookup the entry + * the path to lookup the entry * @return an {@link EntryResponse} which contains information about the entry * @throws RepositoryException - * on fail + * on fail * @throws PasswordInputCanceledException - * if the user canceled the login dialog + * if the user canceled the login dialog */ EntryResponse getEntry(String path) throws PasswordInputCanceledException, RepositoryException; @@ -59,12 +57,12 @@ public interface RemoteContentManager { * Deletes a remote entry specified by the provided path * * @param path - * the path of the entry to be deleted + * the path of the entry to be deleted * @return a response which indicates whether the deletion was successful * @throws RepositoryException - * on fail + * on fail * @throws PasswordInputCanceledException - * if the user canceled the login dialog + * if the user canceled the login dialog */ Response deleteEntry(String path) throws PasswordInputCanceledException, RepositoryException; @@ -72,14 +70,14 @@ public interface RemoteContentManager { * Renames a remote repository entry * * @param path - * the current path of the entry + * the current path of the entry * @param newName - * the new entry name + * the new entry name * @return a response which indicates whether the renaming was successful * @throws RepositoryException - * on fail + * on fail * @throws PasswordInputCanceledException - * if the user canceled the login dialog + * if the user canceled the login dialog */ EntryResponse rename(String path, String newName) throws PasswordInputCanceledException, RepositoryException; @@ -87,30 +85,29 @@ public interface RemoteContentManager { * Moves an entry to a new path * * @param oldPath - * the current (old) path of the entry + * the current (old) path of the entry * @param newPath - * the new path of the entry + * the new path of the entry * @return a response which indicates whether the moving was successful * @throws RepositoryException - * on fail + * on fail * @throws PasswordInputCanceledException - * if the user canceled the login dialog + * if the user canceled the login dialog */ EntryResponse move(String oldPath, String newPath) throws PasswordInputCanceledException, RepositoryException; /** - * * Creates a new folder at the specified path * * @param path - * the path of the parent folder + * the path of the parent folder * @param name - * the name of the new folder + * the name of the new folder * @return a response which indicates whether the moving was successful * @throws RepositoryException - * on fail + * on fail * @throws PasswordInputCanceledException - * if the user canceled the login dialog + * if the user canceled the login dialog */ EntryResponse makeFolder(String path, String name) throws PasswordInputCanceledException, RepositoryException; @@ -118,12 +115,12 @@ public interface RemoteContentManager { * Retrieves the contents of a folder * * @param path - * the path of the folder + * the path of the folder * @return a response which contains information about the folder contents * @throws RepositoryException - * on fail + * on fail * @throws PasswordInputCanceledException - * if the user canceled the login dialog + * if the user canceled the login dialog */ FolderContentsResponse getFolderContents(String path) throws PasswordInputCanceledException, RepositoryException; @@ -131,14 +128,14 @@ public interface RemoteContentManager { * Creates a new (empty) blob entry at the specified path for the specified name * * @param path - * the path of the new blob entry + * the path of the new blob entry * @param name - * the name of the blob entry + * the name of the blob entry * @return a response which indicates whether the creation was successful * @throws RepositoryException - * on fail + * on fail * @throws PasswordInputCanceledException - * if the user canceled the login dialog + * if the user canceled the login dialog */ EntryResponse createBlob(String path, String name) throws PasswordInputCanceledException, RepositoryException; @@ -146,16 +143,16 @@ public interface RemoteContentManager { * Stores a process XML at the specified path. * * @param path - * the path of the process + * the path of the process * @param processXML - * the process XML + * the process XML * @param lastTimestamp - * the change timestamp + * the change timestamp * @return a response which indicates whether the storing was successful * @throws RepositoryException - * on fail + * on fail * @throws PasswordInputCanceledException - * if the user canceled the login dialog + * if the user canceled the login dialog */ Response storeProcess(String path, String processXML, XMLGregorianCalendar lastTimestamp) throws PasswordInputCanceledException, RepositoryException; @@ -164,14 +161,14 @@ Response storeProcess(String path, String processXML, XMLGregorianCalendar lastT * Queries the server for process contents. * * @param path - * the path to the process + * the path to the process * @param revision - * the revision of the process to ask for + * the revision of the process to ask for * @return a response which contains information about the process content * @throws RepositoryException - * on fail + * on fail * @throws PasswordInputCanceledException - * if the user canceled the login dialog + * if the user canceled the login dialog */ ProcessContentsResponse getProcessContents(String path, int revision) throws PasswordInputCanceledException, RepositoryException; @@ -180,12 +177,12 @@ ProcessContentsResponse getProcessContents(String path, int revision) throws Pas * Starts a new process revision * * @param path - * the path to the process + * the path to the process * @return a response which indiciates whether starting a new revision was successful * @throws RepositoryException - * on fail + * on fail * @throws PasswordInputCanceledException - * if the user canceled the login dialog + * if the user canceled the login dialog */ Response startNewRevision(String path) throws PasswordInputCanceledException, RepositoryException; @@ -194,9 +191,9 @@ ProcessContentsResponse getProcessContents(String path, int revision) throws Pas * * @return a list that contains all currently available group names * @throws RepositoryException - * on fail + * on fail * @throws PasswordInputCanceledException - * if the user canceled the login dialog + * if the user canceled the login dialog */ List getAllGroupNames() throws PasswordInputCanceledException, RepositoryException; @@ -204,14 +201,14 @@ ProcessContentsResponse getProcessContents(String path, int revision) throws Pas * Modifies the access rights for a server entry. * * @param path - * the path to the entry + * the path to the entry * @param accessRights - * the new access rights + * the new access rights * @return a response which indicates whether the change was successful * @throws RepositoryException - * on fail + * on fail * @throws PasswordInputCanceledException - * if the user canceled the login dialog + * if the user canceled the login dialog */ Response setAccessRights(String path, List accessRights) throws PasswordInputCanceledException, RepositoryException; @@ -220,12 +217,12 @@ Response setAccessRights(String path, List accessRights) throws Pa * Queries the server for current access rights for a remote entry. * * @param path - * the path of the entry + * the path of the entry * @return the list of access rights for the entry specified by the path * @throws RepositoryException - * on fail + * on fail * @throws PasswordInputCanceledException - * if the user canceled the login dialog + * if the user canceled the login dialog */ List getAccessRights(String path) throws PasswordInputCanceledException, RepositoryException; @@ -233,5 +230,4 @@ Response setAccessRights(String path, List accessRights) throws Pa * @return the {@link BindingProvider} for the content manager */ BindingProvider getBindingProvider(); - } diff --git a/src/main/java/com/rapidminer/repository/internal/remote/RemoteCreateVaultInformation.java b/src/main/java/com/rapidminer/repository/internal/remote/RemoteCreateVaultInformation.java new file mode 100644 index 000000000..26236010a --- /dev/null +++ b/src/main/java/com/rapidminer/repository/internal/remote/RemoteCreateVaultInformation.java @@ -0,0 +1,89 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.repository.internal.remote; + +import com.rapidminer.tools.ValidationUtil; + + +/** + * Container object with the necessary information to create a new vault entry using the {@link RemoteRepository} + * + * @author Andreas Timm + * @since 9.3 + */ +public class RemoteCreateVaultInformation { + private String group; + private String name; + private String value; + + /** + * Data to create in the vault + * + * @param group + * the group the info belongs to + * @param name + * the name to be used when injecting this value + * @param value + * the value to be injected + */ + public RemoteCreateVaultInformation(String group, String name, String value) { + this.group = ValidationUtil.requireNonNull(group); + this.name = ValidationUtil.requireNonNull(name); + this.value = value; + } + + /** + * The group the info belongs to + * + * @return name of the group + */ + public String getGroup() { + return group; + } + + private void setGroup(String group) { + this.group = ValidationUtil.requireNonNull(group); + } + + /** + * The name for this info + * + * @return name of this info + */ + public String getName() { + return name; + } + + private void setName(String name) { + this.name = ValidationUtil.requireNonNull(name); + } + + /** + * The value to be injected + * + * @return value for injection + */ + public String getValue() { + return value; + } + + private void setValue(String value) { + this.value = value; + } +} diff --git a/src/main/java/com/rapidminer/repository/internal/remote/RemoteRepository.java b/src/main/java/com/rapidminer/repository/internal/remote/RemoteRepository.java index 48513df34..6af4d5e9d 100644 --- a/src/main/java/com/rapidminer/repository/internal/remote/RemoteRepository.java +++ b/src/main/java/com/rapidminer/repository/internal/remote/RemoteRepository.java @@ -1,18 +1,18 @@ /** * Copyright (C) 2001-2019 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. */ @@ -26,6 +26,7 @@ import com.rapidminer.repository.ConnectionRepository; import com.rapidminer.repository.RepositoryException; import com.rapidminer.tools.PasswordInputCanceledException; +import com.rapidminer.tools.usagestats.ActionStatisticsCollector; /** @@ -40,11 +41,28 @@ */ public interface RemoteRepository extends RemoteFolder, ConnectionRepository { - public static final String TAG_REMOTE_REPOSITORY = "remoteRepository"; + String TAG_REMOTE_REPOSITORY = "remoteRepository"; /** Type of object requested from a server. */ - public static enum EntryStreamType { - METADATA, IOOBJECT, PROCESS, BLOB + enum EntryStreamType { + METADATA, IOOBJECT, PROCESS, BLOB, CONNECTION_INFORMATION, CONNECTION_METADATA + } + + /** Authentication types */ + enum AuthenticationType { + + BASIC(ActionStatisticsCollector.TYPE_REMOTE_REPOSITORY), // user+password + SAML(ActionStatisticsCollector.TYPE_REMOTE_REPOSITORY_SAML); // enterprise SSO + + private final String actionStatisticsType; // for usage stat collection + + private AuthenticationType(String actionStatisticsType) { + this.actionStatisticsType = actionStatisticsType; + } + + public String getActionStatisticsType() { + return actionStatisticsType; + } } /** @@ -219,4 +237,40 @@ HttpURLConnection getHTTPConnection(String pathInfo, String query, boolean preAu */ default boolean isFileExtensionBlacklisted(String originalFilename) throws IOException, RepositoryException {return false;} + /** + * Return authentication type + * + * @return + */ + AuthenticationType getAuthenticationType(); + + /** + * Sets the authentication type to {@link AuthenticationType#BASIC} or + * {@link AuthenticationType#SAML} + * + * @param authenticationType + * + */ + void setAuthenticationType(AuthenticationType authenticationType); + + + /** + * Load Vault information for a {@link com.rapidminer.connection.ConnectionInformation} in the repositoryLocation + * + * @param repositoryLocation + * location of the {@link com.rapidminer.connection.ConnectionInformation} + * @return the information available in the vault for injection + */ + RemoteVaultEntry[] loadVaultInfo(String repositoryLocation) throws RepositoryException; + + /** + * Create a new entry in the vault to add some information to a {@link com.rapidminer.connection.ConnectionInformation} + * object that is already stored in the repository + * + * @param path + * location of the connection information in the repository + * @param entries + * to be set created data entries containing group and name for injection reference and the value to be injected + */ + void createVaultEntry(String path, List entries) throws IOException, RepositoryException; } diff --git a/src/main/java/com/rapidminer/repository/internal/remote/RemoteRepositoryFactory.java b/src/main/java/com/rapidminer/repository/internal/remote/RemoteRepositoryFactory.java index 07172b90e..c92c843f6 100644 --- a/src/main/java/com/rapidminer/repository/internal/remote/RemoteRepositoryFactory.java +++ b/src/main/java/com/rapidminer/repository/internal/remote/RemoteRepositoryFactory.java @@ -1,21 +1,21 @@ /** * Copyright (C) 2001-2019 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.repository.internal.remote; import java.net.URL; @@ -23,6 +23,7 @@ import com.rapidminer.repository.CustomRepositoryFactory; import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryManager; +import com.rapidminer.repository.internal.remote.RemoteRepository.AuthenticationType; /** @@ -48,9 +49,56 @@ public interface RemoteRepositoryFactory extends CustomRepositoryFactory { * the username * @param password * the password + * @param authenticationType + * {@link AuthenticationType#BASIC} or {@link AuthenticationType#SAML} * @return If the provided configuration is working, null will be returned. If it * is not working, an error message will be returned. */ + String checkConfiguration(String name, String repositoryURL, String userName, char[] password, AuthenticationType authenticationType); + + /** + * Creates a new {@link RemoteRepository} instance for the provided parameters + * + * @param baseUrl + * the Server base URL + * @param alias + * the repository alias + * @param username + * the username + * @param password + * the password + * @param authenticationType + * {@link AuthenticationType#BASIC} or {@link AuthenticationType#SAML} + * @param shouldSave + * defines whether the {@link RepositoryManager} should save the + * {@link RemoteRepository} when storing {@link Repository} configurations + * @return the created {@link RemoteRepository} instance + * @throws RepositoryException + * if connection to the {@link RemoteRepository} isn't possible with the provided + * parameters + */ + RemoteRepository create(URL baseUrl, String alias, String username, char[] password, AuthenticationType authenticationType, boolean shouldSave) + throws RepositoryException; + + /** + * Checks if the provided configuration works. If it is working, null will be + * returned. If it is not working, an error message will be returned. + * + * @param name + * the repository name + * @param repositoryURL + * the URL of the Server repository + * @param userName + * the username + * @param password + * the password + * @return If the provided configuration is working, null will be returned. If it + * is not working, an error message will be returned. + * + * @deprecated removing in favor of + * {@link RemoteRepositoryFactory#checkConfiguration(String, String, String, char[], RemoteRepository.AuthenticationType)} + */ + @Deprecated String checkConfiguration(String name, String repositoryURL, String userName, char[] password); /** @@ -71,7 +119,12 @@ public interface RemoteRepositoryFactory extends CustomRepositoryFactory { * @throws RepositoryException * if connection to the {@link RemoteRepository} isn't possible with the provided * parameters + * + * @deprecated removing in favor of + * {@link RemoteRepositoryFactory#create(URL, String, String, char[], RemoteRepository.AuthenticationType, boolean)} */ + @Deprecated RemoteRepository create(URL baseUrl, String alias, String username, char[] password, boolean shouldSave) - throws RepositoryException; + throws RepositoryException; + } diff --git a/src/main/java/com/rapidminer/repository/internal/remote/RemoteVaultEntry.java b/src/main/java/com/rapidminer/repository/internal/remote/RemoteVaultEntry.java new file mode 100644 index 000000000..1d2a3df09 --- /dev/null +++ b/src/main/java/com/rapidminer/repository/internal/remote/RemoteVaultEntry.java @@ -0,0 +1,122 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.repository.internal.remote; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + + +/** + * Container for response data for the RapidMiner Vault entries + * + * @author Andreas Timm + * @since 9.3 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class RemoteVaultEntry { + + /** Ask Server team for details. This Key contains the group. */ + public static class Key { + private String group; + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + } + + /** The Parameter contains injection info like the name, a Key and more */ + public static class Parameter { + private String name; + private boolean encrypted; + private boolean injectable; + private Key key; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isEncrypted() { + return encrypted; + } + + public void setEncrypted(boolean encrypted) { + this.encrypted = encrypted; + } + + public boolean isInjectable() { + return injectable; + } + + public void setInjectable(boolean injectable) { + this.injectable = injectable; + } + + public Key getKey() { + return key; + } + + public void setKey(Key key) { + this.key = key; + } + } + + private String id; + private Parameter parameter; + private String value; + private double updatedAt; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Parameter getParameter() { + return parameter; + } + + public void setParameter(Parameter parameter) { + this.parameter = parameter; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public double getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(double updatedAt) { + this.updatedAt = updatedAt; + } +} \ No newline at end of file diff --git a/src/main/java/com/rapidminer/repository/local/LocalRepository.java b/src/main/java/com/rapidminer/repository/local/LocalRepository.java index 6b9200897..b55449ba0 100644 --- a/src/main/java/com/rapidminer/repository/local/LocalRepository.java +++ b/src/main/java/com/rapidminer/repository/local/LocalRepository.java @@ -19,7 +19,6 @@ package com.rapidminer.repository.local; import java.io.File; - import javax.swing.event.EventListenerList; import org.w3c.dom.Document; @@ -38,13 +37,15 @@ import com.rapidminer.repository.gui.RepositoryConfigurationPanel; import com.rapidminer.tools.FileSystemService; import com.rapidminer.tools.I18N; +import com.rapidminer.tools.LogService; +import com.rapidminer.tools.SystemInfoUtilities; import com.rapidminer.tools.XMLException; /** * A repository backed by the local file system. Each entry is backed by one or more files. * - * @author Simon Fischer + * @author Simon Fischer, Jan Czogalla * */ public class LocalRepository extends SimpleFolder implements Repository { @@ -93,6 +94,7 @@ public LocalRepository(String name, File root) throws RepositoryException { throw new RepositoryException("Folder '" + root + "' is not writable."); } setRepository(this); + ensureConnectionsFolder(); } public File getRoot() { @@ -112,6 +114,18 @@ public File getFile() { return getRoot(); } + /** + * Get a file associated with this {@link LocalRepository}, specified by the given suffix. + * The returned file is located in the {@link #getRoot() root folder} and it's name is + * a concatenation of {@link #getName()} and the {@code suffix}. + * + * @since 9.3 + */ + @Override + protected File getFile(String suffix) { + return new File(getRoot(), getName() + suffix); + } + public void setRoot(File root) { this.root = root; } @@ -230,11 +244,45 @@ public boolean isConfigurable() { return true; } + @Override + public boolean supportsConnections() { + return true; + } + @Override public RepositoryConfigurationPanel makeConfigurationPanel() { return new LocalRepositoryPanel(null, false); } + + /** + * Checks if a Connections folder already exists for this repository and if not, creates it. + */ + private void ensureConnectionsFolder() { + String rootPath = root.getAbsolutePath(); + File connectionsDirectory = new File(rootPath, Folder.CONNECTION_FOLDER_NAME); + if (connectionsDirectory.exists()) { + return; + } + if (SystemInfoUtilities.getOperatingSystem() != SystemInfoUtilities.OperatingSystem.WINDOWS) { + // need to check root folders case insensitive + File[] files = root.listFiles(File::isDirectory); + if (files != null) { + for (File file : files) { + if (Folder.isConnectionsFolderName(file.getName(), true)) { + return; + } + } + } else { + LogService.getRoot().severe(() -> I18N.getErrorMessage("repository.create_connections_failed", getName())); + } + } + + if (!connectionsDirectory.mkdirs()) { + LogService.getRoot().severe(() -> I18N.getErrorMessage("repository.create_connections_failed", getName())); + } + } + /** * Returns the folder which, by default, contains RM repositories, e.g. .RapidMiner/repositories */ diff --git a/src/main/java/com/rapidminer/repository/local/SimpleBlobEntry.java b/src/main/java/com/rapidminer/repository/local/SimpleBlobEntry.java index 7cdcca95e..ec75b52c4 100644 --- a/src/main/java/com/rapidminer/repository/local/SimpleBlobEntry.java +++ b/src/main/java/com/rapidminer/repository/local/SimpleBlobEntry.java @@ -18,7 +18,6 @@ */ package com.rapidminer.repository.local; -import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -27,60 +26,32 @@ import java.io.OutputStream; import com.rapidminer.repository.BlobEntry; -import com.rapidminer.repository.Folder; import com.rapidminer.repository.RepositoryException; /** * Reference on BLOB entries in the repository. * - * @author Simon Fischer + * @author Simon Fischer, Jan Czogalla */ public class SimpleBlobEntry extends SimpleDataEntry implements BlobEntry { - private static final String BLOB_SUFFIX = ".blob"; - public SimpleBlobEntry(String name, SimpleFolder containingFolder, LocalRepository localRepository) throws RepositoryException { super(name, containingFolder, localRepository); // create physical file here, otherwise it will not really exist and for example cause // errors in the Binary Import Wizard - if (!getFile().exists()) { + if (!getDataFile().exists()) { try { - getFile().createNewFile(); + getDataFile().createNewFile(); } catch (IOException e) { - throw new RepositoryException(e.getMessage()); + throw new RepositoryException(e); } } } - private File getFile() { - return new File(((SimpleFolder) getContainingFolder()).getFile(), getName() + BLOB_SUFFIX); - } - - @Override - public long getDate() { - return getFile().lastModified(); - } - - @Override - public long getSize() { - return getFile().length(); - } - - @Override - public void delete() throws RepositoryException { - getFile().delete(); - super.delete(); - } - - @Override - protected void handleRename(String newName) throws RepositoryException { - renameFile(getFile(), newName); - } - @Override - protected void handleMove(Folder newParent, String newName) throws RepositoryException { - moveFile(getFile(), ((SimpleFolder) newParent).getFile(), newName, BLOB_SUFFIX); + public String getSuffix() { + return BLOB_SUFFIX; } @Override @@ -91,9 +62,9 @@ public String getMimeType() { @Override public InputStream openInputStream() throws RepositoryException { try { - return new FileInputStream(getFile()); + return new FileInputStream(getDataFile()); } catch (FileNotFoundException e) { - throw new RepositoryException("Cannot open stream from '" + getFile() + "': " + e, e); + throw new RepositoryException("Cannot open stream from '" + getDataFile() + "': " + e, e); } } @@ -101,9 +72,9 @@ public InputStream openInputStream() throws RepositoryException { public OutputStream openOutputStream(String mimeType) throws RepositoryException { putProperty("mimetype", mimeType); try { - return new FileOutputStream(getFile()); + return new FileOutputStream(getDataFile()); } catch (IOException e) { - throw new RepositoryException("Cannot open stream from '" + getFile() + "': " + e, e); + throw new RepositoryException("Cannot open stream from '" + getDataFile() + "': " + e, e); } } } diff --git a/src/main/java/com/rapidminer/repository/local/SimpleConnectionEntry.java b/src/main/java/com/rapidminer/repository/local/SimpleConnectionEntry.java new file mode 100644 index 000000000..90bafcacd --- /dev/null +++ b/src/main/java/com/rapidminer/repository/local/SimpleConnectionEntry.java @@ -0,0 +1,171 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.repository.local; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.util.logging.Level; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.ConnectionInformationContainerIOObject; +import com.rapidminer.connection.ConnectionInformationSerializer; +import com.rapidminer.connection.configuration.ConnectionConfiguration; +import com.rapidminer.operator.Annotations; +import com.rapidminer.operator.IOObject; +import com.rapidminer.operator.ports.metadata.ConnectionInformationMetaData; +import com.rapidminer.operator.ports.metadata.MetaData; +import com.rapidminer.repository.ConnectionEntry; +import com.rapidminer.repository.Entry; +import com.rapidminer.repository.RepositoryException; +import com.rapidminer.tools.LogService; + + +/** + * The Repository {@link Entry} containing {@link ConnectionInformation}. + * + * @author Andreas Timm, Jan Czogalla + * @since 9.3 + */ +public class SimpleConnectionEntry extends SimpleIOObjectEntry implements ConnectionEntry { + + /** The .properties entry for the {@link com.rapidminer.connection.configuration.ConnectionConfiguration#getType connection type} */ + private static final String PROPERTY_CONNECTION_TYPE = "connection-type"; + /** The cached {@link com.rapidminer.connection.configuration.ConnectionConfiguration#getType connection type} */ + private String cachedConnectionType; + + /** + * Construct an instance to access the {@link ConnectionInformation} + * + * @param name + * filename of this {@link Entry}, used when reading and writing the {@link ConnectionInformation}, {@link MetaData} and {@link Annotations} + * @param containingFolder + * parent {@link com.rapidminer.repository.Folder} + * @param repository + * the {@link com.rapidminer.repository.Repository} this {@link Entry} belongs to + * @throws RepositoryException + * if initializing an empty {@link SimpleConnectionEntry} failed + */ + public SimpleConnectionEntry(String name, SimpleFolder containingFolder, LocalRepository repository) throws RepositoryException { + super(name, containingFolder, repository); + } + + @Override + public String getSuffix() { + return CON_SUFFIX; + } + + + @Override + public String getDefaultDescription() { + return "Connection entry."; + } + + @Override + public void storeConnectionInformation(ConnectionInformation connectionInformation) throws RepositoryException { + if (connectionInformation == null) { + throw new RepositoryException("No connection provided for storing"); + } + storeData(new ConnectionInformationContainerIOObject(connectionInformation), null, null); + } + + @Override + public String getConnectionType() { + if (cachedConnectionType != null) { + return cachedConnectionType; + } + // first try from properties file + cachedConnectionType = getProperty(PROPERTY_CONNECTION_TYPE); + if (cachedConnectionType != null) { + return cachedConnectionType; + } + // if not yet defined, retrieve it from meta data and store in properties + try { + cachedConnectionType = ((ConnectionInformationMetaData) retrieveMetaData()).getConnectionType(); + if (cachedConnectionType != null) { + putProperty(PROPERTY_CONNECTION_TYPE, cachedConnectionType); + } + return cachedConnectionType; + } catch (RepositoryException e) { + return null; + } + } + + @Override + protected String getMetaDataSuffix() { + return CON_MD_SUFFIX; + } + + @Override + protected void writeDataToFile(IOObject data, FileOutputStream fos) throws IOException, RepositoryException { + if (data instanceof ConnectionInformationContainerIOObject) { + ConnectionInformation connectionInformation = ((ConnectionInformationContainerIOObject) data).getConnectionInformation(); + ConnectionInformationSerializer.LOCAL.serialize(connectionInformation, fos); + } else { + throw new IOException("Mismatched IOObject, expected connection but was " + data.getClass()); + } + } + + @Override + protected void writeMetaDataToFile(MetaData md, FileOutputStream fos) throws IOException { + if (md instanceof ConnectionInformationMetaData) { + ConnectionConfiguration configuration = ((ConnectionInformationMetaData) md).getConfiguration(); + ConnectionInformationSerializer.LOCAL.writeJson(fos, configuration); + } + } + + @Override + protected MetaData readMetaDataObject(File metaDataFile) throws IOException, ClassNotFoundException { + try (FileReader fr = new FileReader(getMetaDataFile())) { + return new ConnectionInformationMetaData(ConnectionInformationSerializer.LOCAL.loadConfiguration(fr)); + } + } + + @Override + protected IOObject readDataFromFile(FileInputStream fis) throws IOException { + return new ConnectionInformationContainerIOObject( + ConnectionInformationSerializer.LOCAL.loadConnection(fis, getLocation())); + } + + @Override + protected void checkMetaDataFile() { + final File metaDataFile = getMetaDataFile(); + if (!metaDataFile.exists()) { + IOObject data; + try { + data = retrieveData(null); + } catch (RepositoryException e) { + // data is not there anymore, we can ignore this and also not need to care about MD anymore now + return; + } + + try { + MetaData md = MetaData.forIOObject(data); + // Save MetaData + try (FileOutputStream fos = new FileOutputStream(metaDataFile)) { + writeMetaDataToFile(md, fos); + } + } catch (IOException e) { + LogService.getRoot().log(Level.WARNING, "Cannot write meta data to '" + metaDataFile + "': " + e, e); + } + } + } +} diff --git a/src/main/java/com/rapidminer/repository/local/SimpleDataEntry.java b/src/main/java/com/rapidminer/repository/local/SimpleDataEntry.java index 8d7d762e4..e0dfd49c1 100644 --- a/src/main/java/com/rapidminer/repository/local/SimpleDataEntry.java +++ b/src/main/java/com/rapidminer/repository/local/SimpleDataEntry.java @@ -1,28 +1,32 @@ /** * Copyright (C) 2001-2019 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.repository.local; +import java.io.File; + import com.rapidminer.repository.DataEntry; +import com.rapidminer.repository.Folder; +import com.rapidminer.repository.RepositoryException; /** - * @author Simon Fischer + * @author Simon Fischer, Jan Czogalla */ public abstract class SimpleDataEntry extends SimpleEntry implements DataEntry { @@ -30,6 +34,24 @@ public SimpleDataEntry(String name, SimpleFolder containingFolder, LocalReposito super(name, containingFolder, localRepository); } + /** + * Suffix for the specialized entry type, like {@value SimpleIOObjectEntry#IOO_SUFFIX} + * + * @return the file suffix + * @since 9.3 + */ + public abstract String getSuffix(); + + @Override + public long getDate() { + return getDataFile().lastModified(); + } + + @Override + public long getSize() { + return getDataFile().length(); + } + @Override public String getDescription() { return getName(); @@ -39,4 +61,33 @@ public String getDescription() { public int getRevision() { return 1; } + + @Override + public void delete() throws RepositoryException { + if (getDataFile().exists()) { + getDataFile().delete(); + } + super.delete(); + } + + /** + * Retrieves the main file associated with this {@link DataEntry}. + * + * @see #getFile(String) + * @see #getSuffix() + * @since 9.3 + */ + protected File getDataFile() { + return getFile(getSuffix()); + } + + @Override + protected void handleRename(String newName) throws RepositoryException { + renameFile(getDataFile(), newName); + } + + @Override + protected void handleMove(Folder newParent, String newName) throws RepositoryException { + moveFile(getDataFile(), ((SimpleFolder) newParent).getFile(), newName, getSuffix()); + } } diff --git a/src/main/java/com/rapidminer/repository/local/SimpleEntry.java b/src/main/java/com/rapidminer/repository/local/SimpleEntry.java index a71b26233..f6be49463 100644 --- a/src/main/java/com/rapidminer/repository/local/SimpleEntry.java +++ b/src/main/java/com/rapidminer/repository/local/SimpleEntry.java @@ -28,26 +28,30 @@ import java.util.List; import java.util.Properties; import java.util.logging.Level; - import javax.swing.Action; +import com.rapidminer.repository.ConnectionEntry; import com.rapidminer.repository.DataEntry; import com.rapidminer.repository.Entry; import com.rapidminer.repository.Folder; import com.rapidminer.repository.MalformedRepositoryLocationException; +import com.rapidminer.repository.Repository; +import com.rapidminer.repository.RepositoryConnectionsFolderDuplicateException; import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; +import com.rapidminer.repository.RepositoryNotConnectionsFolderException; +import com.rapidminer.repository.RepositoryStoreOtherInConnectionsFolderException; +import com.rapidminer.repository.RepositoryTools; import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; import com.rapidminer.tools.container.Pair; /** - * @author Simon Fischer + * @author Simon Fischer, Jan Czogalla */ public abstract class SimpleEntry implements Entry { - protected static final String PROPERTIES_SUFFIX = ".properties"; protected static final String DOT = "."; private Properties properties; @@ -203,7 +207,14 @@ boolean renameFile(File file, String newBaseName, String extensionSuffix, File t * {@link RepositoryException} will be thrown. */ private void checkRename(Folder newParent, String newName) throws RepositoryException { - + // only connection entries can be moved inside special folder, folders need special handling + if (RepositoryTools.isInSpecialConnectionsFolder(newParent) && !(this instanceof ConnectionEntry) && !(this instanceof Folder)) { + throw new RepositoryStoreOtherInConnectionsFolderException(Folder.MESSAGE_CONNECTION_FOLDER); + } else if (!newParent.isSpecialConnectionsFolder() && (this instanceof ConnectionEntry)) { + throw new RepositoryNotConnectionsFolderException(Folder.MESSAGE_CONNECTION_CREATION); + } else if (newParent instanceof Repository && Folder.isConnectionsFolderName(newName, false)) { + throw new RepositoryConnectionsFolderDuplicateException(Folder.MESSAGE_CONNECTION_FOLDER_DUPLICATE); + } if (!RepositoryLocation.isNameValid(newName)) { throw new RepositoryException(I18N.getMessage(I18N.getErrorBundle(), "repository.illegal_entry_name", newName, getLocation())); @@ -212,14 +223,14 @@ private void checkRename(Folder newParent, String newName) throws RepositoryExce if (containingFolder != null) { List dataEntries = newParent.getDataEntries(); for (Entry entry : dataEntries) { - if (entry.getName().equals(newName)) { + if (entry.getName().equalsIgnoreCase(newName)) { throw new RepositoryException(I18N.getMessage(I18N.getErrorBundle(), "repository.repository_entry_with_same_name_already_exists", newName)); } } List subfolders = newParent.getSubfolders(); for (Folder folder : subfolders) { - if (folder.getName().equals(newName)) { + if (folder.getName().equalsIgnoreCase(newName)) { throw new RepositoryException(I18N.getMessage(I18N.getErrorBundle(), "repository.repository_entry_with_same_name_already_exists", newName)); } @@ -228,7 +239,7 @@ private void checkRename(Folder newParent, String newName) throws RepositoryExce } private Pair extractNameAndSuffix(File file) { - String name = null; + String name; String suffix = null; String fileName = file.getName(); int dot = fileName.lastIndexOf(DOT); @@ -240,7 +251,7 @@ private Pair extractNameAndSuffix(File file) { name = fileName.substring(0, dot); suffix = fileName.substring(dot + 1); } - return new Pair(name, suffix); + return new Pair<>(name, suffix); } /** @@ -397,12 +408,19 @@ protected String getProperty(String key) { return getProperties().getProperty(key); } + /** + * Get a file associated with this {@link Entry}, specified by the given suffix. + * The returned file is located in the {@link #getContainingFolder() containing folder} and it's name is + * a concatenation of {@link #getName()} and the {@code suffix}. + * + * @since 9.3 + */ + protected File getFile(String suffix) { + return new File(((SimpleFolder) getContainingFolder()).getFile(), getName() + suffix); + } + private File getPropertiesFile() { - if (getContainingFolder() != null) { - return new File(((SimpleFolder) getContainingFolder()).getFile(), getName() + PROPERTIES_SUFFIX); - } else { - return new File(getRepository().getRoot(), getName() + PROPERTIES_SUFFIX); - } + return getFile(PROPERTIES_SUFFIX); } @Override diff --git a/src/main/java/com/rapidminer/repository/local/SimpleFolder.java b/src/main/java/com/rapidminer/repository/local/SimpleFolder.java index bf6eb3f2b..7e837f230 100644 --- a/src/main/java/com/rapidminer/repository/local/SimpleFolder.java +++ b/src/main/java/com/rapidminer/repository/local/SimpleFolder.java @@ -1,53 +1,79 @@ /** * Copyright (C) 2001-2019 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.repository.local; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.ConnectionInformationContainerIOObject; import com.rapidminer.operator.IOObject; import com.rapidminer.operator.Operator; import com.rapidminer.repository.BlobEntry; +import com.rapidminer.repository.ConnectionEntry; import com.rapidminer.repository.DataEntry; import com.rapidminer.repository.DateEntry; +import com.rapidminer.repository.EntryCreator; import com.rapidminer.repository.Folder; import com.rapidminer.repository.IOObjectEntry; import com.rapidminer.repository.ProcessEntry; +import com.rapidminer.repository.Repository; +import com.rapidminer.repository.RepositoryConnectionsFolderImmutableException; import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; +import com.rapidminer.repository.RepositoryNotConnectionsFolderException; +import com.rapidminer.repository.RepositoryStoreOtherInConnectionsFolderException; import com.rapidminer.repository.RepositoryTools; +import com.rapidminer.tools.ConsumerWithThrowable; import com.rapidminer.tools.I18N; import com.rapidminer.tools.ProgressListener; import com.rapidminer.tools.Tools; /** - * @author Simon Fischer + * @author Simon Fischer, Jan Czogalla */ public class SimpleFolder extends SimpleEntry implements Folder, DateEntry { + /** + * A map of {@link EntryCreator}, one for each {@link SimpleDataEntry}. + * @since 9.3 + */ + private static final Map> CREATOR_MAP; + static { + Map> entryCreators = new HashMap<>(); + entryCreators.put(IOObjectEntry.IOO_SUFFIX, SimpleIOObjectEntry::new); + entryCreators.put(ProcessEntry.RMP_SUFFIX, SimpleProcessEntry::new); + entryCreators.put(BlobEntry.BLOB_SUFFIX, SimpleBlobEntry::new); + // ignore connections outside connection folder + entryCreators.put(ConnectionEntry.CON_SUFFIX, (s, f, r) -> f.isSpecialConnectionsFolder() ? new SimpleConnectionEntry(s, f, r) : null); + CREATOR_MAP = Collections.unmodifiableMap(entryCreators); + } + private List data; private List folders; @@ -61,25 +87,31 @@ public SimpleFolder(String name, SimpleFolder parent, LocalRepository repository protected void mkdir() throws RepositoryException { File file = getFile(); - if (!file.exists()) { - if (!file.mkdirs()) { - throw new RepositoryException("Cannot create repository folder at '" + file + "'."); - } + if (!file.exists() && !file.mkdirs()) { + throw new RepositoryException("Cannot create repository folder at '" + file + "'."); } } @Override protected void handleRename(String newName) throws RepositoryException { + if (isSpecialConnectionsFolder()) { + throw new RepositoryConnectionsFolderImmutableException(MESSAGE_CONNECTION_FOLDER_CHANGE); + } renameFile(getFile(), newName); } @Override protected void handleMove(Folder newParent, String newName) throws RepositoryException { + if (isSpecialConnectionsFolder()) { + throw new RepositoryConnectionsFolderImmutableException(MESSAGE_CONNECTION_FOLDER_CHANGE); + } else if (newParent.isSpecialConnectionsFolder()) { + throw new RepositoryStoreOtherInConnectionsFolderException(MESSAGE_CONNECTION_FOLDER); + } moveFile(getFile(), ((SimpleFolder) newParent).getFile(), newName, ""); } protected File getFile() { - return new File(((SimpleFolder) getContainingFolder()).getFile(), getName()); + return getFile(""); } @Override @@ -132,62 +164,65 @@ private void ensureLoaded() throws RepositoryException { if (isLoaded()) { return; } - data = new ArrayList(); - folders = new ArrayList(); + data = new ArrayList<>(); + folders = new ArrayList<>(); File fileFolder = getFile(); - if (fileFolder != null && fileFolder.exists()) { - File[] listFiles = fileFolder.listFiles(); - for (File file : listFiles) { - if (file.isHidden()) { - continue; - } - if (file.isDirectory()) { - folders.add(new SimpleFolder(file.getName(), this, getRepository())); - } else if (file.getName().endsWith(".ioo")) { - data.add(new SimpleIOObjectEntry(file.getName().substring(0, file.getName().length() - 4), this, - getRepository())); - } else if (file.getName().endsWith(".rmp")) { - data.add(new SimpleProcessEntry(file.getName().substring(0, file.getName().length() - 4), this, - getRepository())); - - } else if (file.getName().endsWith(".blob")) { - data.add(new SimpleBlobEntry(file.getName().substring(0, file.getName().length() - 5), this, - getRepository())); + if (fileFolder == null || !fileFolder.exists()) { + return; + } + File[] listFiles = fileFolder.listFiles(); + if (listFiles == null) { + throw new RepositoryException("Could not read folder contents of " + fileFolder); + } + if (listFiles.length == 0) { + // no files found, nothing left to do + return; + } + for (File file : listFiles) { + if (file.isHidden()) { + continue; + } + if (file.isDirectory()) { + folders.add(new SimpleFolder(file.getName(), this, getRepository())); + } else { + String name = file.getName(); + int dotPos = name.lastIndexOf('.'); + if (dotPos >= 0) { + String suffix = name.substring(dotPos); + name = name.substring(0, dotPos); + SimpleDataEntry entry = CREATOR_MAP.getOrDefault(suffix, EntryCreator.nullCreator()).create(name, this, getRepository()); + if (entry != null) { + data.add(entry); + } } - Collections.sort(data, RepositoryTools.SIMPLE_NAME_COMPARATOR); - Collections.sort(folders, RepositoryTools.SIMPLE_NAME_COMPARATOR); } } + data.sort(RepositoryTools.SIMPLE_NAME_COMPARATOR); + folders.sort(RepositoryTools.SIMPLE_NAME_COMPARATOR); } @Override public IOObjectEntry createIOObjectEntry(String name, IOObject ioobject, Operator callingOperator, ProgressListener l) throws RepositoryException { - // check for possible invalid name - if (!RepositoryLocation.isNameValid(name)) { - throw new RepositoryException( - I18N.getMessage(I18N.getErrorBundle(), "repository.illegal_entry_name", name, getLocation())); - } - - IOObjectEntry entry = new SimpleIOObjectEntry(name, this, getRepository()); - - acquireWriteLock(); - try { - ensureLoaded(); - data.add(entry); - } finally { - releaseWriteLock(); - } - - if (ioobject != null) { - entry.storeData(ioobject, null, l); + if (ioobject instanceof ConnectionInformationContainerIOObject) { + return createConnectionEntry(name, ((ConnectionInformationContainerIOObject) ioobject).getConnectionInformation()); + } else if (RepositoryTools.isInSpecialConnectionsFolder(this)) { + throw new RepositoryStoreOtherInConnectionsFolderException(MESSAGE_CONNECTION_FOLDER); + } + ConsumerWithThrowable storeAction; + if (ioobject == null) { + storeAction = x -> {}; + } else { + storeAction = entry -> entry.storeData(ioobject, null, l); } - getRepository().fireEntryAdded(entry, this); - return entry; + return createEntry(name, SimpleIOObjectEntry::new, storeAction); } @Override public Folder createFolder(String name) throws RepositoryException { + if (RepositoryTools.isInSpecialConnectionsFolder(this)) { + throw new RepositoryStoreOtherInConnectionsFolderException(MESSAGE_CONNECTION_FOLDER); + } // check for possible invalid name if (!RepositoryLocation.isNameValid(name)) { throw new RepositoryException( @@ -199,9 +234,11 @@ public Folder createFolder(String name) throws RepositoryException { try { ensureLoaded(); for (Folder folder : folders) { - // folder with the same name (no matter if they have different capitalization) must + // folder with the same name (no matter if they have different capitalization) + // must // not - // be created + // be + // created if (folder.getName().toLowerCase(Locale.ENGLISH).equals(name.toLowerCase(Locale.ENGLISH))) { throw new RepositoryException( I18N.getMessage(I18N.getErrorBundle(), "repository.repository_folder_already_exists", name)); @@ -279,6 +316,9 @@ private boolean containsEntryNotThreadSafe(String name) { @Override public void delete() throws RepositoryException { + if (isSpecialConnectionsFolder()) { + throw new RepositoryConnectionsFolderImmutableException(MESSAGE_CONNECTION_FOLDER_CHANGE); + } if (!Tools.delete(getFile())) { throw new RepositoryException("Cannot delete directory"); } else { @@ -321,60 +361,64 @@ void addChild(SimpleEntry child) throws RepositoryException { @Override public ProcessEntry createProcessEntry(String name, String processXML) throws RepositoryException { - // check for possible invalid name - if (!RepositoryLocation.isNameValid(name)) { - throw new RepositoryException( - I18N.getMessage(I18N.getErrorBundle(), "repository.illegal_entry_name", name, getLocation())); + if (RepositoryTools.isInSpecialConnectionsFolder(this)) { + throw new RepositoryStoreOtherInConnectionsFolderException(MESSAGE_CONNECTION_FOLDER); } + return createEntry(name, SimpleProcessEntry::new, entry -> entry.storeXML(processXML)); + } - SimpleProcessEntry entry = null; - acquireWriteLock(); - try { - ensureLoaded(); - entry = new SimpleProcessEntry(name, this, getRepository()); - data.add(entry); - try { - entry.storeXML(processXML); - } catch (RepositoryException e) { - data.remove(entry); - throw e; - } - } finally { - releaseWriteLock(); - } - if (entry != null) { - getRepository().fireEntryAdded(entry, this); + @Override + public ConnectionEntry createConnectionEntry(String name, ConnectionInformation connectionInformation) throws RepositoryException { + if (!isSpecialConnectionsFolder()) { + throw new RepositoryNotConnectionsFolderException(MESSAGE_CONNECTION_CREATION); } - return entry; + return createEntry(name, SimpleConnectionEntry::new, entry -> entry.storeConnectionInformation(connectionInformation)); } @Override public BlobEntry createBlobEntry(String name) throws RepositoryException { + if (RepositoryTools.isInSpecialConnectionsFolder(this)) { + throw new RepositoryStoreOtherInConnectionsFolderException(MESSAGE_CONNECTION_FOLDER); + } + return createEntry(name, SimpleBlobEntry::new, x -> {}); + } + + /** + * Creates a new {@link SimpleDataEntry} with the given name, using the specified creator and executing the store action + * after the entry was created and added. This method streamlines all four creation methods into one. + * + * @since 9.3 + */ + private T createEntry(String name, EntryCreator creator, + ConsumerWithThrowable storeAction) throws RepositoryException { // check for possible invalid name if (!RepositoryLocation.isNameValid(name)) { throw new RepositoryException( I18N.getMessage(I18N.getErrorBundle(), "repository.illegal_entry_name", name, getLocation())); } - - BlobEntry entry = null; + T entry; acquireWriteLock(); try { ensureLoaded(); - entry = new SimpleBlobEntry(name, this, getRepository()); + entry = creator.create(name, this, getRepository()); data.add(entry); + try { + storeAction.acceptWithException(entry); + } catch (RepositoryException e) { + data.remove(entry); + throw e; + } } finally { releaseWriteLock(); } - if (entry != null) { - getRepository().fireEntryAdded(entry, this); - } + getRepository().fireEntryAdded(entry, this); return entry; } @Override public boolean canRefreshChild(String childName) throws RepositoryException { // check existence of properties file - childName += SimpleEntry.PROPERTIES_SUFFIX; + childName += PROPERTIES_SUFFIX; File propFile = new File(getFile(), childName); if (!propFile.exists()) { return false; @@ -393,6 +437,24 @@ public boolean canRefreshChild(String childName) throws RepositoryException { return true; } + /** + * Checks whether this folder is the special "Connections" folder in which only connections can be stored. Opposed + * to the overwritten default method, this check is case insensitive to not break existing connections folders with + * different capitalization. + * + * @return {@code true} if this folder is called 'Connections' in any capitalization; {@code false} otherwise + */ + @Override + public boolean isSpecialConnectionsFolder() { + // on Windows, you can have a "connections" folder or some other capitalization instead of "Connections" + // therefore, we have to account for this by simply checking case-insensitive as we cannot just create a new "Connections" folder as we could on Unix + Folder containingFolder = getContainingFolder(); + // folder is one step below the repository + return containingFolder instanceof Repository + // and has the special name (case-insensitive) + && Folder.isConnectionsFolderName(getName(), false); + } + private void acquireReadLock() throws RepositoryException { try { readLock.lock(); diff --git a/src/main/java/com/rapidminer/repository/local/SimpleIOObjectEntry.java b/src/main/java/com/rapidminer/repository/local/SimpleIOObjectEntry.java index ca7e65cfe..d550eed6e 100644 --- a/src/main/java/com/rapidminer/repository/local/SimpleIOObjectEntry.java +++ b/src/main/java/com/rapidminer/repository/local/SimpleIOObjectEntry.java @@ -1,21 +1,21 @@ /** * Copyright (C) 2001-2019 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.repository.local; import java.io.BufferedInputStream; @@ -23,6 +23,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; @@ -47,13 +48,10 @@ * Stores IOObject in a file. Either as IOO serialized files using {@link ExampleSetToStream} where * appropriate. * - * @author Simon Fischer + * @author Simon Fischer, Jan Czogalla */ public class SimpleIOObjectEntry extends SimpleDataEntry implements IOObjectEntry { - private static final String MD_SUFFIX = ".md"; - private static final String IOO_SUFFIX = ".ioo"; - private static final String PROPERTY_IOOBJECT_CLASS = "ioobject-class"; private WeakReference metaData = null; @@ -63,12 +61,28 @@ public SimpleIOObjectEntry(String name, SimpleFolder containingFolder, LocalRepo super(name, containingFolder, repository); } - private File getDataFile() { - return new File(((SimpleFolder) getContainingFolder()).getFile(), getName() + IOO_SUFFIX); + @Override + public String getSuffix() { + return IOO_SUFFIX; + } + + + /** + * Suffix for the specialized {@link MetaData} of this type, like {@value #MD_SUFFIX}. + * + * @since 9.3 + */ + protected String getMetaDataSuffix() { + return MD_SUFFIX; } + /** + * Returns the file associated with this entry's {@link MetaData}. + * + * @see #getMetaDataSuffix() + */ protected File getMetaDataFile() { - return new File(((SimpleFolder) getContainingFolder()).getFile(), getName() + MD_SUFFIX); + return getFile(getMetaDataSuffix()); } @Override @@ -79,9 +93,8 @@ public IOObject retrieveData(ProgressListener l) throws RepositoryException { } File dataFile = getDataFile(); if (dataFile.exists()) { - try (FileInputStream fis = new FileInputStream(dataFile); - BufferedInputStream in = new BufferedInputStream(fis)) { - return (IOObject) IOObjectSerializer.getInstance().deserialize(in); + try (FileInputStream fis = new FileInputStream(dataFile)) { + return readDataFromFile(fis); } catch (Exception e) { throw new RepositoryException("Cannot load data from '" + dataFile + "': " + e, e); } @@ -90,6 +103,19 @@ public IOObject retrieveData(ProgressListener l) throws RepositoryException { } } + /** + * Read the actual IOObject from the given {@link FileInputStream}. + * + * @throws IOException + * if an error occurs + * @since 9.3 + */ + protected IOObject readDataFromFile(FileInputStream fis) throws IOException { + try (BufferedInputStream in = new BufferedInputStream(fis)) { + return (IOObject) IOObjectSerializer.getInstance().deserialize(in); + } + } + @Override public MetaData retrieveMetaData() throws RepositoryException { if (metaData != null) { @@ -100,28 +126,57 @@ public MetaData retrieveMetaData() throws RepositoryException { } // otherwise metaData == null OR get() == null -> re-read MetaData readObject; + checkMetaDataFile(); File metaDataFile = getMetaDataFile(); - if (metaDataFile.exists()) { - try (FileInputStream fis = new FileInputStream(metaDataFile); - ObjectInputStream objectIn = new RMObjectInputStream(fis)) { - readObject = (MetaData) objectIn.readObject(); - this.metaData = new WeakReference<>(readObject); - if (readObject instanceof ExampleSetMetaData) { - for (AttributeMetaData amd : ((ExampleSetMetaData) readObject).getAllAttributes()) { - if (amd.isNominal()) { - amd.shrinkValueSet(); - } + if (!metaDataFile.exists()) { + throw new RepositoryException("Meta data file '" + metaDataFile + " does not exist'."); + } + try { + readObject = readMetaDataObject(metaDataFile); + this.metaData = new WeakReference<>(readObject); + if (readObject instanceof ExampleSetMetaData) { + for (AttributeMetaData amd : ((ExampleSetMetaData) readObject).getAllAttributes()) { + if (amd.isNominal()) { + amd.shrinkValueSet(); } } - } catch (Exception e) { - throw new RepositoryException("Cannot load meta data from '" + metaDataFile + "': " + e, e); } - } else { - throw new RepositoryException("Meta data file '" + metaDataFile + " does not exist'."); + } catch (Exception e) { + throw new RepositoryException("Cannot load meta data from '" + metaDataFile + "': " + e, e); } return readObject; } + /** + * Before handing out the metadata from a file this method is invoked to perform a check on the filesystem level, for + * instance if creation of a missing metadata file should be done. + * + * @since 9.3 + */ + protected void checkMetaDataFile() { + // noop + } + + /** + * Re-usability for {@link MetaData} retrieval by overriding this method that returns the {@link MetaData} which + * should be contained in the given file. + * + * @param metaDataFile + * {@link File} to load that contains previously stored {@link MetaData} + * @return the {@link MetaData} object loaded from the metaDataFile + * @throws IOException + * if reading failed + * @throws ClassNotFoundException + * if reading failed + * @since 9.3 + */ + protected MetaData readMetaDataObject(File metaDataFile) throws IOException, ClassNotFoundException { + try (FileInputStream fis = new FileInputStream(metaDataFile); + ObjectInputStream objectIn = new RMObjectInputStream(fis)) { + return (MetaData) objectIn.readObject(); + } + } + @Override public void storeData(IOObject data, Operator callingOperator, ProgressListener l) throws RepositoryException { if (l != null) { @@ -131,8 +186,8 @@ public void storeData(IOObject data, Operator callingOperator, ProgressListener boolean existed = getDataFile().exists(); MetaData md = MetaData.forIOObject(data); // Serialize Non-ExampleSets as IOO - try (FileOutputStream fos = new FileOutputStream(getDataFile()); OutputStream out = new BufferedOutputStream(fos)) { - IOObjectSerializer.getInstance().serialize(out, data); + try (FileOutputStream fos = new FileOutputStream(getDataFile())) { + writeDataToFile(data, fos); if (l != null) { l.setCompleted(75); } @@ -140,9 +195,8 @@ public void storeData(IOObject data, Operator callingOperator, ProgressListener throw new RepositoryException("Cannot store data at '" + getDataFile() + "': " + e, e); } // Save MetaData - try (FileOutputStream fos = new FileOutputStream(getMetaDataFile()); - ObjectOutputStream mdOut = new ObjectOutputStream(fos)) { - mdOut.writeObject(md); + try (FileOutputStream fos = new FileOutputStream(getMetaDataFile())) { + writeMetaDataToFile(md, fos); if (l != null) { l.setCompleted(90); } @@ -162,34 +216,49 @@ public void storeData(IOObject data, Operator callingOperator, ProgressListener } } + /** + * Takes care of the actual storing of the {@link IOObject} in a file. + * @since 9.3 + */ + protected void writeDataToFile(IOObject data, FileOutputStream fos) throws IOException, RepositoryException { + try (OutputStream out = new BufferedOutputStream(fos)) { + IOObjectSerializer.getInstance().serialize(out, data); + } + } + + /** + * Takes care of the actual storing of the {@link MetaData} in a file. + * @since 9.3 + */ + protected void writeMetaDataToFile(MetaData md, FileOutputStream fos) throws IOException { + try (ObjectOutputStream mdOut = new ObjectOutputStream(fos)) { + mdOut.writeObject(md); + } + } + @Override public String getDescription() { if (metaData != null) { MetaData md = metaData.get(); if (md != null) { return md.getDescription(); - } else { - return "Simple entry."; } - } else { - return "Simple entry."; } + return getDefaultDescription(); } - @Override - public long getSize() { - if (getDataFile().exists()) { - return getDataFile().length(); - } else { - return 0; - } + /** + * Get a description for this entry. + * + * @return very short description, basically the name of this entry type + * @since 9.3 + */ + protected String getDefaultDescription() { + return "Simple entry."; } @Override public void delete() throws RepositoryException { - if (getDataFile().exists()) { - getDataFile().delete(); - } if (getMetaDataFile().exists()) { getMetaDataFile().delete(); } @@ -198,19 +267,14 @@ public void delete() throws RepositoryException { @Override protected void handleRename(String newName) throws RepositoryException { - renameFile(getDataFile(), newName); + super.handleRename(newName); renameFile(getMetaDataFile(), newName); } @Override protected void handleMove(Folder newParent, String newName) throws RepositoryException { - moveFile(getDataFile(), ((SimpleFolder) newParent).getFile(), newName, IOO_SUFFIX); - moveFile(getMetaDataFile(), ((SimpleFolder) newParent).getFile(), newName, MD_SUFFIX); - } - - @Override - public long getDate() { - return getDataFile().lastModified(); + super.handleMove(newParent, newName); + moveFile(getMetaDataFile(), ((SimpleFolder) newParent).getFile(), newName, getMetaDataSuffix()); } @Override @@ -221,35 +285,34 @@ public boolean willBlock() { @SuppressWarnings("unchecked") @Override public Class getObjectClass() { - if (dataObjectClass == null) { - // first try from properties file - String className = getProperty(PROPERTY_IOOBJECT_CLASS); - if (className != null) { - try { - dataObjectClass = (Class) Class.forName(className); - return dataObjectClass; - } catch (ClassNotFoundException e) { - try { - dataObjectClass = (Class) Class.forName(className, false, - Plugin.getMajorClassLoader()); - return dataObjectClass; - } catch (ClassNotFoundException e1) { - return null; - } - } - } else { - // if not yet defined, retrieve it from meta data and store in properties + if (dataObjectClass != null) { + return dataObjectClass; + } + // first try from properties file + String className = getProperty(PROPERTY_IOOBJECT_CLASS); + if (className != null) { + try { + dataObjectClass = (Class) Class.forName(className); + return dataObjectClass; + } catch (ClassNotFoundException e) { try { - dataObjectClass = retrieveMetaData().getObjectClass(); - if (dataObjectClass != null) { - putProperty(PROPERTY_IOOBJECT_CLASS, dataObjectClass.getName()); - } + dataObjectClass = (Class) Class.forName(className, false, + Plugin.getMajorClassLoader()); return dataObjectClass; - } catch (RepositoryException e) { + } catch (ClassNotFoundException e1) { return null; } } } - return dataObjectClass; + // if not yet defined, retrieve it from meta data and store in properties + try { + dataObjectClass = retrieveMetaData().getObjectClass(); + if (dataObjectClass != null) { + putProperty(PROPERTY_IOOBJECT_CLASS, dataObjectClass.getName()); + } + return dataObjectClass; + } catch (RepositoryException e) { + return null; + } } } diff --git a/src/main/java/com/rapidminer/repository/local/SimpleProcessEntry.java b/src/main/java/com/rapidminer/repository/local/SimpleProcessEntry.java index 8cb8d4609..6a546177a 100644 --- a/src/main/java/com/rapidminer/repository/local/SimpleProcessEntry.java +++ b/src/main/java/com/rapidminer/repository/local/SimpleProcessEntry.java @@ -1,24 +1,23 @@ /** * Copyright (C) 2001-2019 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.repository.local; -import java.io.File; import java.io.IOException; import com.rapidminer.RepositoryProcessLocation; @@ -30,12 +29,10 @@ /** - * @author Simon Fischer + * @author Simon Fischer, Jan Czogalla */ public class SimpleProcessEntry extends SimpleDataEntry implements ProcessEntry { - private static final String RMP_SUFFIX = ".rmp"; - public SimpleProcessEntry(String name, SimpleFolder containingFolder, LocalRepository repository) { super(name, containingFolder, repository); } @@ -43,43 +40,28 @@ public SimpleProcessEntry(String name, SimpleFolder containingFolder, LocalRepos @Override public String retrieveXML() throws RepositoryException { try { - return Tools.readTextFile(getFile()); + return Tools.readTextFile(getDataFile()); } catch (IOException e) { - throw new RepositoryException("Cannot read " + getFile() + ": " + e, e); + throw new RepositoryException("Cannot read " + getDataFile() + ": " + e, e); } } @Override public void storeXML(String xml) throws RepositoryException { try { - boolean existed = getFile().exists(); - Tools.writeTextFile(getFile(), xml); + boolean existed = getDataFile().exists(); + Tools.writeTextFile(getDataFile(), xml); if (existed) { getRepository().fireEntryChanged(this); } } catch (IOException e) { - throw new RepositoryException("Cannot write " + getFile() + ": " + e, e); + throw new RepositoryException("Cannot write " + getDataFile() + ": " + e, e); } } - private File getFile() { - return new File(((SimpleFolder) getContainingFolder()).getFile(), getName() + RMP_SUFFIX); - } - - @Override - public int getRevision() { - return 1; - } - - @Override - public long getSize() { - return getFile().length(); - } - @Override - public void delete() throws RepositoryException { - getFile().delete(); - super.delete(); + public String getSuffix() { + return RMP_SUFFIX; } @Override @@ -87,21 +69,6 @@ public String getDescription() { return "Local process"; } - @Override - protected void handleRename(String newName) throws RepositoryException { - renameFile(getFile(), newName); - } - - @Override - protected void handleMove(Folder newParent, String newName) throws RepositoryException { - moveFile(getFile(), ((SimpleFolder) newParent).getFile(), newName, RMP_SUFFIX); - } - - @Override - public long getDate() { - return getFile().lastModified(); - } - @Override public boolean rename(String newName) throws RepositoryException { boolean shouldResetLocation = RapidMinerGUI.isMainFrameProcessLocation(getLocation()); diff --git a/src/main/java/com/rapidminer/repository/resource/ResourceBlobEntry.java b/src/main/java/com/rapidminer/repository/resource/ResourceBlobEntry.java index 81f6a6f0c..2fb12c3af 100644 --- a/src/main/java/com/rapidminer/repository/resource/ResourceBlobEntry.java +++ b/src/main/java/com/rapidminer/repository/resource/ResourceBlobEntry.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ package com.rapidminer.repository.resource; import java.io.IOException; @@ -27,7 +45,7 @@ protected ResourceBlobEntry(ResourceFolder parent, String name, String resource, @Override public InputStream openInputStream() throws RepositoryException { - return getResourceStream(".blob"); + return getResourceStream(BLOB_SUFFIX); } @Override @@ -38,7 +56,7 @@ public OutputStream openOutputStream(String mimeType) throws RepositoryException @Override public String getMimeType() { if (mimeType == null) { - try (InputStream in = getResourceStream(".properties")) { + try (InputStream in = getResourceStream(PROPERTIES_SUFFIX)) { Properties props = new Properties(); props.loadFromXML(in); mimeType = props.getProperty("mimetype"); diff --git a/src/main/java/com/rapidminer/repository/resource/ResourceConnectionEntry.java b/src/main/java/com/rapidminer/repository/resource/ResourceConnectionEntry.java new file mode 100644 index 000000000..f6c232e3f --- /dev/null +++ b/src/main/java/com/rapidminer/repository/resource/ResourceConnectionEntry.java @@ -0,0 +1,80 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.repository.resource; + +import java.io.IOException; +import java.io.InputStream; + +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.ConnectionInformationContainerIOObject; +import com.rapidminer.connection.ConnectionInformationSerializer; +import com.rapidminer.operator.IOObject; +import com.rapidminer.operator.ports.metadata.ConnectionInformationMetaData; +import com.rapidminer.operator.ports.metadata.MetaData; +import com.rapidminer.repository.ConnectionEntry; +import com.rapidminer.repository.RepositoryException; + + +/** + * Resource based {@link com.rapidminer.connection.ConnectionInformation}, read only + * + * @author Andreas Timm + * @since 9.3 + */ +public class ResourceConnectionEntry extends ResourceIOObjectEntry implements ConnectionEntry { + + ResourceConnectionEntry(ResourceFolder parent, String name, String resource, ResourceRepository repository) { + super(parent, name, resource, repository); + } + + @Override + protected String getSuffix() { + return CON_SUFFIX; + } + + @Override + protected String getMetaDataSuffix() { + return CON_MD_SUFFIX; + } + + @Override + protected IOObject readDataObject(InputStream in) throws IOException { + return new ConnectionInformationContainerIOObject( + ConnectionInformationSerializer.LOCAL.loadConnection(in, getLocation())); + } + + @Override + protected MetaData readMetaDataObject(InputStream in) throws IOException { + return new ConnectionInformationMetaData(ConnectionInformationSerializer.LOCAL.loadConfiguration(in)); + } + + @Override + public void storeConnectionInformation(ConnectionInformation connectionInformation) throws RepositoryException { + throw new RepositoryException("This is a read-only sample connection entry. Cannot store connection here."); + } + + @Override + public String getConnectionType() { + try { + return ((ConnectionInformationMetaData) retrieveMetaData()).getConnectionType(); + } catch (RepositoryException e) { + return null; + } + } +} diff --git a/src/main/java/com/rapidminer/repository/resource/ResourceFolder.java b/src/main/java/com/rapidminer/repository/resource/ResourceFolder.java index 3def2bb81..3820c111d 100644 --- a/src/main/java/com/rapidminer/repository/resource/ResourceFolder.java +++ b/src/main/java/com/rapidminer/repository/resource/ResourceFolder.java @@ -1,36 +1,41 @@ /** * Copyright (C) 2001-2019 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.repository.resource; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import com.rapidminer.connection.ConnectionInformation; import com.rapidminer.operator.IOObject; import com.rapidminer.operator.Operator; import com.rapidminer.repository.BlobEntry; +import com.rapidminer.repository.ConnectionEntry; import com.rapidminer.repository.DataEntry; import com.rapidminer.repository.Entry; +import com.rapidminer.repository.EntryCreator; import com.rapidminer.repository.Folder; import com.rapidminer.repository.IOObjectEntry; import com.rapidminer.repository.ProcessEntry; @@ -46,6 +51,20 @@ */ public class ResourceFolder extends ResourceEntry implements Folder { + /** + * A map of {@link EntryCreator}, one for each {@link ResourceDataEntry}. + * @since 9.3 + */ + private static final Map> CREATOR_MAP; + static { + Map> creatorMap = new HashMap<>(); + creatorMap.put(BlobEntry.BLOB_SUFFIX, (l, f, r) -> new ResourceBlobEntry(f, l[0], l[1], r)); + creatorMap.put(ProcessEntry.RMP_SUFFIX, (l, f, r) -> new ResourceProcessEntry(f, l[0], l[1], r)); + creatorMap.put(IOObjectEntry.IOO_SUFFIX, (l, f, r) -> new ResourceIOObjectEntry(f, l[0], l[1], r)); + creatorMap.put(ConnectionEntry.CON_SUFFIX, (l, f, r) -> new ResourceConnectionEntry(f, l[0], l[1], r)); + CREATOR_MAP = Collections.unmodifiableMap(creatorMap); + } + private List folders; private List data; @@ -103,7 +122,7 @@ public Folder createFolder(String name) throws RepositoryException { @Override public IOObjectEntry createIOObjectEntry(String name, IOObject ioobject, Operator callingOperator, - ProgressListener newParam) throws RepositoryException { + ProgressListener newParam) throws RepositoryException { throw new RepositoryException("This is a read-only sample repository. Cannot create new entries."); } @@ -112,6 +131,11 @@ public ProcessEntry createProcessEntry(String name, String processXML) throws Re throw new RepositoryException("This is a read-only sample repository. Cannot create new entries."); } + @Override + public ConnectionEntry createConnectionEntry(String name, ConnectionInformation connectionInformation) throws RepositoryException { + throw new RepositoryException("This is a read-only sample repository. Cannot create new entries."); + } + @Override public List getDataEntries() throws RepositoryException { acquireReadLock(); @@ -159,7 +183,7 @@ protected void ensureLoaded() throws RepositoryException { * if an error occurs * @since 9.0 */ - protected void ensureLoaded(List folders, List data) throws RepositoryException{ + protected void ensureLoaded(List folders, List data) throws RepositoryException { try (InputStream in = getResourceStream("/CONTENTS"); InputStreamReader reader = new InputStreamReader(in, "UTF-8")) { String[] lines = Tools.readTextFile(reader).split("\n"); @@ -179,25 +203,17 @@ protected void ensureLoaded(List folders, List data) throws R String suffix = ""; if (suffixStart >= 0) { nameWOExt = name.substring(0, suffixStart); - suffix = name.substring(suffixStart + 1); + suffix = name.substring(suffixStart); } - DataEntry entry; - switch (suffix) { - case "rmp": - entry = new ResourceProcessEntry(this, nameWOExt, getPath() + "/" + nameWOExt, getRepository()); - break; - case "ioo": - entry = new ResourceIOObjectEntry(this, nameWOExt, getPath() + "/" + nameWOExt, getRepository()); - break; - case "blob": - entry = new ResourceBlobEntry(this, nameWOExt, getPath() + "/" + nameWOExt, getRepository()); - break; - default: - entry = null; + if (!ConnectionEntry.CON_SUFFIX.equals(suffix) || isSpecialConnectionsFolder()) { + //ignore connection entries outside special folder + DataEntry entry = CREATOR_MAP.getOrDefault(suffix, EntryCreator.nullCreator()) + .create(new String[]{nameWOExt, getPath() + "/" + nameWOExt}, this, getRepository()); + if (entry != null) { + data.add(entry); + } else { errorSource = name; - } - if (entry != null) { - data.add(entry); + } } } else { errorSource = line; diff --git a/src/main/java/com/rapidminer/repository/resource/ResourceIOObjectEntry.java b/src/main/java/com/rapidminer/repository/resource/ResourceIOObjectEntry.java index fbb2d1868..ce1220a6d 100644 --- a/src/main/java/com/rapidminer/repository/resource/ResourceIOObjectEntry.java +++ b/src/main/java/com/rapidminer/repository/resource/ResourceIOObjectEntry.java @@ -1,21 +1,21 @@ /** * Copyright (C) 2001-2019 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.repository.resource; import java.io.IOException; @@ -31,16 +31,13 @@ import com.rapidminer.operator.ports.metadata.MetaDataFactory; import com.rapidminer.operator.tools.IOObjectSerializer; import com.rapidminer.repository.IOObjectEntry; -import com.rapidminer.repository.Repository; import com.rapidminer.repository.RepositoryException; import com.rapidminer.tools.LogService; import com.rapidminer.tools.ProgressListener; /** - * * @author Simon Fischer, Jan Czogalla - * */ public class ResourceIOObjectEntry extends ResourceDataEntry implements IOObjectEntry { @@ -61,19 +58,40 @@ public IOObject retrieveData(ProgressListener l) throws RepositoryException { l.setTotal(100); l.setCompleted(10); } - try (InputStream in = getResourceStream(".ioo")) { - return (IOObject) IOObjectSerializer.getInstance().deserialize(in); + try (InputStream in = getResourceStream(getSuffix())) { + return readDataObject(in); } catch (IOException e) { - throw new RepositoryException("Cannot load data from '" + getResource() + ".ioo': " + e, e); + throw new RepositoryException("Cannot load data from '" + getResource() + getSuffix() + "': " + e, e); } } + /** + * Read the actual IOObject from the given {@link InputStream}. + * + * @param in the input stream to read from + * @throws IOException + * if an error occurs + * @since 9.3 + */ + protected IOObject readDataObject(InputStream in) throws IOException { + return (IOObject) IOObjectSerializer.getInstance().deserialize(in); + } + + /** + * Get the suffix for this resource object. + * + * @return the resource file suffix with a leading dot + * @since 9.3 + */ + protected String getSuffix() { + return IOO_SUFFIX; + } + @Override public MetaData retrieveMetaData() throws RepositoryException { if (metaData == null) { - try (InputStream in = getResourceStream(".md"); - ObjectInputStream objectIn = new ObjectInputStream(in)) { - this.metaData = (MetaData) objectIn.readObject(); + try (InputStream in = getResourceStream(getMetaDataSuffix())) { + this.metaData = readMetaDataObject(in); if (this.metaData instanceof ExampleSetMetaData) { for (AttributeMetaData amd : ((ExampleSetMetaData) metaData).getAllAttributes()) { if (amd.isNominal()) { @@ -86,12 +104,22 @@ public MetaData retrieveMetaData() throws RepositoryException { LogService.getRoot().log(Level.WARNING, "com.rapidminer.repository.resource.ResourceIOObjectEntry.missing_metadata", getName()); metaData = MetaDataFactory.getInstance().createMetaDataforIOObject(retrieveData(null), false); } catch (IOException | ClassNotFoundException e) { - throw new RepositoryException("Cannot load meta data from '" + getResource() + ".md': " + e, e); + throw new RepositoryException("Cannot load meta data from '" + getResource() + getMetaDataSuffix() + "': " + e, e); } } return metaData; } + /** + * File ending for the resource that contains the {@link MetaData} + * + * @return the suffix with a leading dot + * @since 9.3 + */ + protected String getMetaDataSuffix() { + return MD_SUFFIX; + } + @Override public void storeData(IOObject data, Operator callingOperator, ProgressListener l) throws RepositoryException { throw new RepositoryException("This is a read-only sample data entry. Cannot store data here."); @@ -110,4 +138,22 @@ public Class getObjectClass() { return null; } } + + /** + * Read the {@link MetaData} object. + * + * @param in + * the stream to read the meta data from + * @return the {@link MetaData} for this {@link ResourceIOObjectEntry} + * @throws IOException + * in case reading did not work + * @throws ClassNotFoundException + * if deserialization is not possible due to the missing class + * @since 9.3 + */ + protected MetaData readMetaDataObject(InputStream in) throws IOException, ClassNotFoundException { + try (ObjectInputStream objectIn = new ObjectInputStream(in)) { + return (MetaData) objectIn.readObject(); + } + } } diff --git a/src/main/java/com/rapidminer/repository/resource/ResourceProcessEntry.java b/src/main/java/com/rapidminer/repository/resource/ResourceProcessEntry.java index 5b721592e..777404d4c 100644 --- a/src/main/java/com/rapidminer/repository/resource/ResourceProcessEntry.java +++ b/src/main/java/com/rapidminer/repository/resource/ResourceProcessEntry.java @@ -40,7 +40,7 @@ protected ResourceProcessEntry(ResourceFolder parent, String name, String resour @Override public String retrieveXML() throws RepositoryException { - try (InputStream in = getResourceStream(".rmp"); + try (InputStream in = getResourceStream(RMP_SUFFIX); InputStreamReader isr = new InputStreamReader(in)) { return Tools.readTextFile(isr); } catch (IOException e) { diff --git a/src/main/java/com/rapidminer/repository/resource/ZipResourceConnectionEntry.java b/src/main/java/com/rapidminer/repository/resource/ZipResourceConnectionEntry.java new file mode 100644 index 000000000..d72aed9de --- /dev/null +++ b/src/main/java/com/rapidminer/repository/resource/ZipResourceConnectionEntry.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.repository.resource; + +import java.io.InputStream; + +import com.rapidminer.repository.RepositoryException; + + +/** + * Read a {@link com.rapidminer.connection.ConnectionInformation} inside a ZipResource + * + * @author Andreas Timm + * @since 9.3 + */ +public class ZipResourceConnectionEntry extends ResourceConnectionEntry { + + private final ZipStreamResource zipStream; + + ZipResourceConnectionEntry(ResourceFolder parent, String name, String resource, ResourceRepository repository, ZipStreamResource zipStream) { + super(parent, name, resource, repository); + this.zipStream = zipStream; + } + + @Override + protected InputStream getResourceStream(String suffix) throws RepositoryException { + return zipStream.getStream(getName(), getResource(), suffix); + } + +} diff --git a/src/main/java/com/rapidminer/repository/resource/ZipResourceFolder.java b/src/main/java/com/rapidminer/repository/resource/ZipResourceFolder.java index b7f3a5832..931cbccc5 100644 --- a/src/main/java/com/rapidminer/repository/resource/ZipResourceFolder.java +++ b/src/main/java/com/rapidminer/repository/resource/ZipResourceFolder.java @@ -1,31 +1,39 @@ /** * Copyright (C) 2001-2019 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.repository.resource; import java.io.IOException; import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; +import com.rapidminer.repository.BlobEntry; +import com.rapidminer.repository.ConnectionEntry; import com.rapidminer.repository.DataEntry; +import com.rapidminer.repository.EntryCreator; import com.rapidminer.repository.Folder; +import com.rapidminer.repository.IOObjectEntry; +import com.rapidminer.repository.ProcessEntry; import com.rapidminer.repository.RepositoryException; @@ -38,10 +46,25 @@ */ public class ZipResourceFolder extends ResourceFolder { + /** + * A map of {@link EntryCreator}, one for each zip version of {@link ResourceDataEntry}. + * @since 9.3 + */ + private static final Map> CREATOR_MAP; + static { + Map> creatorMap = new HashMap<>(); + creatorMap.put(BlobEntry.BLOB_SUFFIX, (l, f, r) -> new ZipResourceBlobEntry(f, l[0], l[1], r, f.zipStream)); + creatorMap.put(ProcessEntry.RMP_SUFFIX, (l, f, r) -> new ZipResourceProcessEntry(f, l[0], l[1], r, f.zipStream)); + creatorMap.put(IOObjectEntry.IOO_SUFFIX, (l, f, r) -> new ZipResourceIOObjectEntry(f, l[0], l[1], r, f.zipStream)); + // ignore connections outside connection folder + creatorMap.put(ConnectionEntry.CON_SUFFIX, (l, f, r) -> f.isSpecialConnectionsFolder() ? new ZipResourceConnectionEntry(f, l[0], l[1], r, f.zipStream) : null); + CREATOR_MAP = Collections.unmodifiableMap(creatorMap); + } + private final ZipStreamResource zipStream; protected ZipResourceFolder(ResourceFolder parent, String name, ZipStreamResource zipStream, String parentPath, - ResourceRepository repository) { + ResourceRepository repository) { super(parent, name, parentPath + "/" + name, repository); this.zipStream = zipStream; } @@ -51,23 +74,21 @@ protected void ensureLoaded(List folders, List data) throws R try (ZipInputStream zip = zipStream.getStream()) { ZipEntry entry; while ((entry = zip.getNextEntry()) != null) { - if (entry.isDirectory() || entry.getName().replaceFirst("/", "").contains("/")) { - continue; - } - if (zipStream.getStreamPath() != null && !entry.getName().startsWith(zipStream.getStreamPath())) { + if (entry.isDirectory() || entry.getName().replaceFirst("/", "").contains("/") + || zipStream.getStreamPath() != null && !entry.getName().startsWith(zipStream.getStreamPath())) { continue; } String entryName = entry.getName(); - String dataEntryName = Paths.get(entryName).getFileName().toString().split("\\.")[0]; - if (entryName.endsWith(".ioo")) { - data.add(new ZipResourceIOObjectEntry(this, dataEntryName, getPath() + "/" + dataEntryName, - getRepository(), zipStream)); - } else if (entryName.endsWith(".rmp")) { - data.add(new ZipResourceProcessEntry(this, dataEntryName, getPath() + "/" + dataEntryName, - getRepository(), zipStream)); - } else if (entryName.endsWith(".blob")) { - data.add(new ZipResourceBlobEntry(this, dataEntryName, getPath() + "/" + dataEntryName, - getRepository(), zipStream)); + String[] split = Paths.get(entryName).getFileName().toString().split("\\."); + String suffix = ""; + if (split.length > 1) { + suffix = '.' + split[split.length - 1]; + } + String dataEntryName = split[0]; + ResourceDataEntry dataEntry = CREATOR_MAP.getOrDefault(suffix, EntryCreator.nullCreator()) + .create(new String[]{dataEntryName, getPath() + "/" + dataEntryName}, this, getRepository()); + if (dataEntry != null) { + data.add(dataEntry); } } } catch (IOException e) { diff --git a/src/main/java/com/rapidminer/repository/resource/ZipResourceIOObjectEntry.java b/src/main/java/com/rapidminer/repository/resource/ZipResourceIOObjectEntry.java index b0cf0debe..c83eb52ee 100644 --- a/src/main/java/com/rapidminer/repository/resource/ZipResourceIOObjectEntry.java +++ b/src/main/java/com/rapidminer/repository/resource/ZipResourceIOObjectEntry.java @@ -53,10 +53,4 @@ protected ZipResourceIOObjectEntry(ResourceFolder parent, String name, String re protected InputStream getResourceStream(String suffix) throws RepositoryException { return zipStream.getStream(getName(), getResource(), suffix); } - - @Override - public boolean willBlock() { - return metaData == null; - } - } diff --git a/src/main/java/com/rapidminer/repository/search/RepositoryGlobalSearch.java b/src/main/java/com/rapidminer/repository/search/RepositoryGlobalSearch.java index d75d786e6..d1a73ef75 100644 --- a/src/main/java/com/rapidminer/repository/search/RepositoryGlobalSearch.java +++ b/src/main/java/com/rapidminer/repository/search/RepositoryGlobalSearch.java @@ -22,8 +22,8 @@ import com.rapidminer.parameter.ParameterTypeBoolean; import com.rapidminer.search.GlobalSearchIndexer; import com.rapidminer.search.GlobalSearchManager; -import com.rapidminer.search.GlobalSearchable; import com.rapidminer.search.GlobalSearchRegistry; +import com.rapidminer.search.GlobalSearchable; /** @@ -36,6 +36,7 @@ public class RepositoryGlobalSearch implements GlobalSearchable { public static final String CATEGORY_ID = "repository"; + public static final String FIELD_CONNECTION_TYPE = "connection_type"; /** property controlling whether full repository indexing is enabled */ public static final String PROPERTY_FULL_REPOSITORY_INDEXING = "rapidminer.search.repository.enable_full_indexing"; diff --git a/src/main/java/com/rapidminer/repository/search/RepositoryGlobalSearchItem.java b/src/main/java/com/rapidminer/repository/search/RepositoryGlobalSearchItem.java index db5c215e6..cc3db2b54 100644 --- a/src/main/java/com/rapidminer/repository/search/RepositoryGlobalSearchItem.java +++ b/src/main/java/com/rapidminer/repository/search/RepositoryGlobalSearchItem.java @@ -36,6 +36,9 @@ public class RepositoryGlobalSearchItem { private String location; private String modified; private String[] attributes; + private String connectionName; + private String connectionType; + private String[] connectionTags; /** * The name of this item. @@ -100,6 +103,36 @@ public String[] getAttributes() { return attributes; } + /** + * The connection name associated with this entry + * + * @return the connection name, may be {@code null} or empty. + * @since 9.3 + */ + public String getConnectionName() { + return connectionName; + } + + /** + * The connection type associated with this entry + * + * @return the connection type, may be {@code null} or empty + * @since 9.3 + */ + public String getConnectionType() { + return connectionType; + } + + /** + * The connection tags associated with this entry + * + * @return the connection tags, may be {@code null} or empty + * @since 9.3 + */ + public String[] getConnectionTags() { + return connectionTags; + } + public RepositoryGlobalSearchItem setName(String name) { this.name = name; return this; @@ -135,6 +168,18 @@ public RepositoryGlobalSearchItem setAttributes(String[] attributes) { return this; } + public void setConnectionName(String connectionName) { + this.connectionName = connectionName; + } + + public void setConnectionType(String connectionType) { + this.connectionType = connectionType; + } + + public void setConnectionTags(String[] connectionTags) { + this.connectionTags = connectionTags; + } + @Override public String toString() { return getLocation(); diff --git a/src/main/java/com/rapidminer/repository/search/RepositoryGlobalSearchManager.java b/src/main/java/com/rapidminer/repository/search/RepositoryGlobalSearchManager.java index db3ff819c..9b7c65303 100644 --- a/src/main/java/com/rapidminer/repository/search/RepositoryGlobalSearchManager.java +++ b/src/main/java/com/rapidminer/repository/search/RepositoryGlobalSearchManager.java @@ -33,11 +33,15 @@ import org.apache.lucene.document.Field; import org.apache.lucene.queryparser.classic.ParseException; +import com.rapidminer.connection.configuration.ConnectionConfiguration; +import com.rapidminer.connection.util.ConnectionI18N; import com.rapidminer.gui.tools.ProgressThread; import com.rapidminer.operator.ports.metadata.AttributeMetaData; +import com.rapidminer.operator.ports.metadata.ConnectionInformationMetaData; import com.rapidminer.operator.ports.metadata.ExampleSetMetaData; import com.rapidminer.operator.ports.metadata.MetaData; import com.rapidminer.operator.ports.metadata.ModelMetaData; +import com.rapidminer.repository.ConnectionEntry; import com.rapidminer.repository.ConnectionListener; import com.rapidminer.repository.ConnectionRepository; import com.rapidminer.repository.DataEntry; @@ -56,6 +60,7 @@ import com.rapidminer.repository.internal.remote.RemoteRepository; import com.rapidminer.repository.resource.ResourceRepository; import com.rapidminer.search.AbstractGlobalSearchManager; +import com.rapidminer.search.GlobalSearchDefaultField; import com.rapidminer.search.GlobalSearchRegistry; import com.rapidminer.search.GlobalSearchResult; import com.rapidminer.search.GlobalSearchResultBuilder; @@ -77,6 +82,9 @@ class RepositoryGlobalSearchManager extends AbstractGlobalSearchManager implemen private static final String API_REST_REMOTE_REPO_DETAILS = "api/rest/globalsearch/repo/details"; private static final String API_REST_REMOTE_REPO_SUMMARY = "api/rest/globalsearch/repo/summary"; + private static final float FIELD_BOOST_CONNECTION_TAG = 0.5f; + private static final float FIELD_BOOST_CONNECTION_TYPE_NAME = 0.25f; + private static final Map ADDITIONAL_FIELDS; private static final String FIELD_TYPE = "type"; private static final String FIELD_PARENT = "parent"; @@ -84,6 +92,10 @@ class RepositoryGlobalSearchManager extends AbstractGlobalSearchManager implemen private static final String FIELD_MODIFIED = "modified"; private static final String FIELD_USER = "user"; private static final String FIELD_ATTRIBUTE = "attribute"; + private static final String FIELD_CONNECTION_NAME = "connection_name"; + private static final String FIELD_CONNECTION_TYPE = RepositoryGlobalSearch.FIELD_CONNECTION_TYPE; + private static final String FIELD_CONNECTION_TYPE_NAME = "connection_type_name"; + private static final String FIELD_CONNECTION_TAGS = "connection_tags"; static { ADDITIONAL_FIELDS = new HashMap<>(); @@ -92,11 +104,16 @@ class RepositoryGlobalSearchManager extends AbstractGlobalSearchManager implemen ADDITIONAL_FIELDS.put(FIELD_MODIFIED, "The timestamp of the last modification of the data, if available. Format: 'YYYY-MM-DD'"); ADDITIONAL_FIELDS.put(FIELD_USER, "The user who last edited the data"); ADDITIONAL_FIELDS.put(FIELD_ATTRIBUTE, "The attributes for ExampleSets and the training set attributes in case of Models"); + ADDITIONAL_FIELDS.put(FIELD_CONNECTION_NAME, "The name of a connection"); + ADDITIONAL_FIELDS.put(FIELD_CONNECTION_TYPE, "The type key of a connection"); + ADDITIONAL_FIELDS.put(FIELD_CONNECTION_TYPE_NAME, "The type name of a connection"); + ADDITIONAL_FIELDS.put(FIELD_CONNECTION_TAGS, "The tags of a connection"); } protected RepositoryGlobalSearchManager() { - super(RepositoryGlobalSearch.CATEGORY_ID, ADDITIONAL_FIELDS); + super(RepositoryGlobalSearch.CATEGORY_ID, ADDITIONAL_FIELDS, new GlobalSearchDefaultField(FIELD_CONNECTION_TAGS, FIELD_BOOST_CONNECTION_TAG), + new GlobalSearchDefaultField(FIELD_CONNECTION_TYPE_NAME, FIELD_BOOST_CONNECTION_TYPE_NAME)); } @Override @@ -355,10 +372,10 @@ private void indexRemoteFolder(final List list, final Folder folder, f list.add(createDocument(item)); } } else { - LogService.getRoot().log(Level.WARNING, "com.rapidminer.repository.global_search.RepositorySearchManager.error.initial_index_error_remote_folder", new Object[] {repository.getName() + path, responseCode}); + LogService.getRoot().log(Level.WARNING, "com.rapidminer.repository.global_search.RepositorySearchManager.error.initial_index_error_remote_folder", new Object[]{repository.getName() + path, responseCode}); } } catch (IOException | RepositoryException e) { - LogService.getRoot().log(Level.WARNING, "com.rapidminer.repository.global_search.RepositorySearchManager.error.initial_index_error_remote_folder", new Object[] {repository.getName() + path, e.getMessage()}); + LogService.getRoot().log(Level.WARNING, "com.rapidminer.repository.global_search.RepositorySearchManager.error.initial_index_error_remote_folder", new Object[]{repository.getName() + path, e.getMessage()}); } } @@ -421,12 +438,12 @@ private void indexRESTFolder(List list, Folder folder, RESTRepository list.add(createDocument(item)); } } else { - LogService.getRoot().log(Level.WARNING, "com.rapidminer.repository.global_search.RepositorySearchManager.error.initial_index_error_rest_folder", new Object[] {repository.getName() + path, responseCode}); + LogService.getRoot().log(Level.WARNING, "com.rapidminer.repository.global_search.RepositorySearchManager.error.initial_index_error_rest_folder", new Object[]{repository.getName() + path, responseCode}); LogService.getRoot().log(Level.WARNING, "com.rapidminer.repository.global_search.RepositorySearchManager.error.initial_index_fallback_rest_folder"); indexFolder(list, folder, fullIndex, pg); } } catch (IOException | RepositoryException e) { - LogService.getRoot().log(Level.WARNING, "com.rapidminer.repository.global_search.RepositorySearchManager.error.initial_index_error_rest_folder", new Object[] {repository.getName() + path, e.getMessage()}); + LogService.getRoot().log(Level.WARNING, "com.rapidminer.repository.global_search.RepositorySearchManager.error.initial_index_error_rest_folder", new Object[]{repository.getName() + path, e.getMessage()}); } } @@ -471,7 +488,23 @@ private RepositoryGlobalSearchItem createItem(final Entry entry, final boolean i } // See if it's an ExampleSet/Model, then try to get its attributes - if (indexMetaData && entry instanceof IOObjectEntry) { + if (entry instanceof ConnectionEntry) { + // Extract connection information from metadata + try { + item.setConnectionName(entry.getName()); + ConnectionEntry conEntry = (ConnectionEntry) entry; + item.setConnectionType(conEntry.getConnectionType()); + ConnectionConfiguration conf = ((ConnectionInformationMetaData) conEntry.retrieveMetaData()).getConfiguration(); + List tags = conf.getTags(); + item.setConnectionTags(tags != null ? tags.toArray(new String[0]) : null); + } catch (RepositoryException e) { + // no metadata available, ignore + } catch (Exception e) { + // no metadata, no connection information + LogService.log(LogService.getRoot(), Level.WARNING, e, "com.rapidminer.repository.global_search.RepositorySearchManager.error.initial_index_error_md_connection_reading", entry.getLocation().getAbsoluteLocation()); + } + } else if (indexMetaData && entry instanceof IOObjectEntry) { + // Extract attributes from example sets try { MetaData md = ((IOObjectEntry) entry).retrieveMetaData(); ExampleSetMetaData exampleSetMetaData = null; @@ -541,13 +574,20 @@ private Document createDocument(final RepositoryGlobalSearchItem item) { // See if it's an ExampleSet/Model, then try to get its attributes String[] attributes = item.getAttributes(); if (attributes != null && attributes.length > 0) { - StringBuilder sb = new StringBuilder(); - for (String attributeName : attributes) { - sb.append(attributeName); - sb.append(' '); + fields.add(GlobalSearchUtilities.INSTANCE.createFieldForTexts(FIELD_ATTRIBUTE, String.join(" ", attributes))); + } - } - fields.add(GlobalSearchUtilities.INSTANCE.createFieldForTexts(FIELD_ATTRIBUTE, sb.toString())); + // Connection fields + if (item.getConnectionName() != null) { + fields.add(GlobalSearchUtilities.INSTANCE.createFieldForTitles(FIELD_CONNECTION_NAME, item.getConnectionName())); + } + if (item.getConnectionTags() != null) { + fields.add(GlobalSearchUtilities.INSTANCE.createFieldForTexts(FIELD_CONNECTION_TAGS, String.join(" ", item.getConnectionTags()))); + } + String connectionType = item.getConnectionType(); + if (connectionType != null) { + fields.add(GlobalSearchUtilities.INSTANCE.createFieldForTitles(FIELD_CONNECTION_TYPE, connectionType)); + fields.add(GlobalSearchUtilities.INSTANCE.createFieldForTitles(FIELD_CONNECTION_TYPE_NAME, ConnectionI18N.getTypeName(connectionType))); } // generic fields @@ -558,7 +598,7 @@ private Document createDocument(final RepositoryGlobalSearchItem item) { fields.add(GlobalSearchUtilities.INSTANCE.createFieldForIdentifiers(FIELD_USER, item.getOwner())); } // absolute repository location is the unique ID for the repository category - return GlobalSearchUtilities.INSTANCE.createDocument(item.getLocation(), item.getName(), fields.toArray(new Field[fields.size()])); + return GlobalSearchUtilities.INSTANCE.createDocument(item.getLocation(), item.getName(), fields.toArray(new Field[0])); } } diff --git a/src/main/java/com/rapidminer/search/GlobalSearchCategory.java b/src/main/java/com/rapidminer/search/GlobalSearchCategory.java index 307326df5..61b381d01 100644 --- a/src/main/java/com/rapidminer/search/GlobalSearchCategory.java +++ b/src/main/java/com/rapidminer/search/GlobalSearchCategory.java @@ -19,7 +19,7 @@ package com.rapidminer.search; /** - * A search category pojo. Contains only the manager instance and the category id. + * A search category pojo. Contains only the manager instance, category id and visibility information. * * @author Marco Boeck * @since 8.1 @@ -30,10 +30,46 @@ public class GlobalSearchCategory { private final GlobalSearchManager manager; + private final boolean visible; + /** + * Use {@link #GlobalSearchCategory(GlobalSearchManager)} instead + * + * @param categoryId + * unused + * @param manager + * the global search manager + * @deprecated since 9.3 + */ + @Deprecated GlobalSearchCategory(String categoryId, GlobalSearchManager manager) { - this.categoryId = categoryId; + this(manager); + } + + /** + * Creates a new GlobalSearchCategory that is visible in the UI + * + * @param manager + * the global search manager + * @since 9.3 + */ + public GlobalSearchCategory(GlobalSearchManager manager) { + this(manager, true); + } + + /** + * Creates a new GlobalSearchCategory + * + * @param manager + * the global search manager + * @param showInUI + * if the category should be shown in global search UI + * @since 9.3 + */ + public GlobalSearchCategory(GlobalSearchManager manager, boolean showInUI) { + this.categoryId = manager.getSearchCategoryId(); this.manager = manager; + this.visible = showInUI; } /** @@ -54,6 +90,16 @@ public String getCategoryId() { return categoryId; } + /** + * Indicates if the given Search category is visible + * + * @return {@code true} if the category should be shown in the global search ui + * @since 9.3 + */ + public boolean isVisible() { + return visible; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -72,4 +118,5 @@ public boolean equals(Object o) { public int hashCode() { return categoryId.hashCode(); } + } diff --git a/src/main/java/com/rapidminer/search/GlobalSearchIndexer.java b/src/main/java/com/rapidminer/search/GlobalSearchIndexer.java index 0e449080f..81d7b048a 100644 --- a/src/main/java/com/rapidminer/search/GlobalSearchIndexer.java +++ b/src/main/java/com/rapidminer/search/GlobalSearchIndexer.java @@ -72,6 +72,8 @@ public enum GlobalSearchIndexer { public void documentsAdded(final String categoryId, final Collection addedDocuments) { GlobalSearchCategory category = GlobalSearchRegistry.INSTANCE.getSearchCategoryById(categoryId); if (category != null) { + setAdditionalFields(addedDocuments, category); + pool.submit(() -> addDocuments(category, addedDocuments)); } } @@ -80,6 +82,8 @@ public void documentsAdded(final String categoryId, final Collection a public void documentsUpdated(final String categoryId, final Collection updatedDocuments) { GlobalSearchCategory category = GlobalSearchRegistry.INSTANCE.getSearchCategoryById(categoryId); if (category != null) { + setAdditionalFields(updatedDocuments, category); + pool.submit(() -> updateDocuments(category, updatedDocuments)); } } @@ -91,6 +95,26 @@ public void documentsRemoved(final String categoryId, final Collection pool.submit(() -> removeDocuments(category, removedDocuments)); } } + + /** + * Sets the category and unique ID fields. + * + * @param documents + * the documents for which to add the fields + * @param category + * the category for which the documents are + */ + private void setAdditionalFields(Collection documents, GlobalSearchCategory category) { + for (Document doc : documents) { + // make sure doc has necessary fields + if (!isDocValid(category.getCategoryId(), doc)) { + continue; + } + // store category id to make searching only for specific categories possible + doc.add(GlobalSearchUtilities.INSTANCE.createFieldForIdentifiers(GlobalSearchUtilities.FIELD_CATEGORY, category.getCategoryId())); + doc.add(GlobalSearchUtilities.INSTANCE.createFieldForIdentifiers(GlobalSearchHandler.FIELD_INTERNAL_UNIQUE_ID, createInternalId(category.getCategoryId(), doc))); + } + } }; @@ -185,16 +209,6 @@ private void removeCategory(final GlobalSearchCategory category) { * the documents to add to the index */ private void addDocuments(final GlobalSearchCategory category, final Collection documents) { - for (Document doc : documents) { - // make sure doc has necessary fields - if (!isDocValid(category.getCategoryId(), doc)) { - continue; - } - // store category id to make searching only for specific categories possible - doc.add(GlobalSearchUtilities.INSTANCE.createFieldForIdentifiers(GlobalSearchUtilities.FIELD_CATEGORY, category.getCategoryId())); - doc.add(GlobalSearchUtilities.INSTANCE.createFieldForIdentifiers(GlobalSearchHandler.FIELD_INTERNAL_UNIQUE_ID, createInternalId(category.getCategoryId(), doc))); - } - try { indexWriter.addDocuments(documents); } catch (Exception e) { @@ -221,10 +235,6 @@ private void updateDocuments(final GlobalSearchCategory category, final Collecti continue; } - // store category id to make searching only for specific categories possible - doc.add(GlobalSearchUtilities.INSTANCE.createFieldForIdentifiers(GlobalSearchUtilities.FIELD_CATEGORY, category.getCategoryId())); - doc.add(GlobalSearchUtilities.INSTANCE.createFieldForIdentifiers(GlobalSearchHandler.FIELD_INTERNAL_UNIQUE_ID, createInternalId(category.getCategoryId(), doc))); - IndexableField field = doc.getField(GlobalSearchHandler.FIELD_INTERNAL_UNIQUE_ID); Term termToUpdate = new Term(field.name(), field.stringValue()); try { diff --git a/src/main/java/com/rapidminer/search/GlobalSearchManager.java b/src/main/java/com/rapidminer/search/GlobalSearchManager.java index 8bf3d07a6..16bee8b22 100644 --- a/src/main/java/com/rapidminer/search/GlobalSearchManager.java +++ b/src/main/java/com/rapidminer/search/GlobalSearchManager.java @@ -89,4 +89,15 @@ public interface GlobalSearchManager { */ GlobalSearchManagerEventHandler getSearchManagerEventHandler(); + /** + * Returns the {@link GlobalSearchCategory} for this Manager. + * If the {@link GlobalSearchCategory#isVisible()} it will be displayed in the Global Search UI as a new category + * and it entries will appear in the All Studio search. + * + * @return the GlobalSearchCategory + */ + default GlobalSearchCategory getSearchCategory() { + return new GlobalSearchCategory(this); + } + } diff --git a/src/main/java/com/rapidminer/search/GlobalSearchRegistry.java b/src/main/java/com/rapidminer/search/GlobalSearchRegistry.java index 72ed83368..42c386fc9 100644 --- a/src/main/java/com/rapidminer/search/GlobalSearchRegistry.java +++ b/src/main/java/com/rapidminer/search/GlobalSearchRegistry.java @@ -91,14 +91,18 @@ public void registerSearchCategory(final GlobalSearchable searchable) { if (searchable == null) { throw new IllegalArgumentException("searchable must not be null!"); } - if (searchable.getSearchManager() == null) { + GlobalSearchManager searchManager = searchable.getSearchManager(); + if (searchManager == null) { throw new IllegalArgumentException("searchable must not return null for the GlobalSearchManager!"); } - GlobalSearchManager searchManager = searchable.getSearchManager(); if (searchManager.getAdditionalDefaultSearchFields() == null) { throw new IllegalArgumentException("getAdditionalDefaultSearchFields() must not return null!"); } - String categoryId = searchManager.getSearchCategoryId(); + GlobalSearchCategory addedCategory = searchManager.getSearchCategory(); + if (addedCategory == null) { + throw new IllegalArgumentException("getSearchCategory() must not return null for the GlobalSearchManager!"); + } + String categoryId = addedCategory.getCategoryId(); if (categoryId == null || categoryId.trim().isEmpty()) { throw new IllegalArgumentException("categoryId must not be null or empty!"); } @@ -110,7 +114,6 @@ public void registerSearchCategory(final GlobalSearchable searchable) { } searchManager.initialize(); - GlobalSearchCategory addedCategory = new GlobalSearchCategory(categoryId, searchManager); map.put(categoryId, addedCategory); fireRegistryEvent(GlobalSearchRegistryEvent.RegistrationEvent.SEARCH_CATEGORY_REGISTERED, addedCategory); LogService.getRoot().log(Level.INFO, "com.rapidminer.search.GlobalSearchRegistry.category_added", addedCategory.getCategoryId()); diff --git a/src/main/java/com/rapidminer/search/GlobalSearchUtilities.java b/src/main/java/com/rapidminer/search/GlobalSearchUtilities.java index 86604fc7c..eaee18d62 100644 --- a/src/main/java/com/rapidminer/search/GlobalSearchUtilities.java +++ b/src/main/java/com/rapidminer/search/GlobalSearchUtilities.java @@ -274,7 +274,7 @@ public Field createFieldForBinary(final String key, final byte[] bytes) { * @return the field which is used for sorting, never {@code null} */ public Field createSortingField(final long value) { - return new SortedNumericDocValuesField("sort", value); + return new SortedNumericDocValuesField(FIELD_SORTING, value); } /** diff --git a/src/main/java/com/rapidminer/studio/concurrency/internal/RecursiveWrapper.java b/src/main/java/com/rapidminer/studio/concurrency/internal/RecursiveWrapper.java index e0c220a17..a4f55f54f 100644 --- a/src/main/java/com/rapidminer/studio/concurrency/internal/RecursiveWrapper.java +++ b/src/main/java/com/rapidminer/studio/concurrency/internal/RecursiveWrapper.java @@ -121,7 +121,7 @@ static List call(List> callables) throws ExecutionException { return Arrays.asList(resultArray); } catch (ProcessStoppedRuntimeException e) { // handle ProcessStoppedRuntimeException as done by StudioConcurrencyContext#collectResults - throw (ExecutionStoppedException) e.getCause(); + throw e; }catch (WrapperRuntimeException e){ // unwrap own wrapped exceptions and wrap into ExecutionException throw new ExecutionException(e.getCause()); diff --git a/src/main/java/com/rapidminer/studio/internal/Resources.java b/src/main/java/com/rapidminer/studio/internal/Resources.java index cda9d13af..e5e2ebc78 100644 --- a/src/main/java/com/rapidminer/studio/internal/Resources.java +++ b/src/main/java/com/rapidminer/studio/internal/Resources.java @@ -29,6 +29,7 @@ import com.rapidminer.security.PluginSandboxPolicy; import com.rapidminer.studio.concurrency.internal.ConcurrencyExecutionService; import com.rapidminer.studio.concurrency.internal.StudioConcurrencyContext; +import com.rapidminer.tools.Tools; /** @@ -75,16 +76,13 @@ private ConcurrencyContext getContext() { */ public static class OverridingContextUserData extends ContextUserData { + /** + * @throws UnsupportedOperationException if used by 3rd parties + */ public OverridingContextUserData(ConcurrencyContext context) { super(context); // make sure this cannot be called without RapidMiner internal permissions - try { - if (System.getSecurityManager() != null) { - AccessController.checkPermission(new RuntimePermission(PluginSandboxPolicy.RAPIDMINER_INTERNAL_PERMISSION)); - } - } catch (AccessControlException e) { - throw new UnsupportedOperationException("Internal API, cannot be called by unauthorized sources."); - } + Tools.requireInternalPermission(); } } diff --git a/src/main/java/com/rapidminer/studio/io/gui/internal/DataImportWizardUtils.java b/src/main/java/com/rapidminer/studio/io/gui/internal/DataImportWizardUtils.java index 8c5bdb13f..e0da3510e 100644 --- a/src/main/java/com/rapidminer/studio/io/gui/internal/DataImportWizardUtils.java +++ b/src/main/java/com/rapidminer/studio/io/gui/internal/DataImportWizardUtils.java @@ -156,7 +156,7 @@ public static DataImportWizardCallback showInResultsCallback() { // Switch to result try { Entry entry = entryLocation.locateEntry(); - if (entry != null && entry instanceof IOObjectEntry) { + if (entry instanceof IOObjectEntry) { OpenAction.showAsResult((IOObjectEntry) entry); } } catch (RepositoryException e) { diff --git a/src/main/java/com/rapidminer/studio/io/gui/internal/steps/AbstractToRepositoryStep.java b/src/main/java/com/rapidminer/studio/io/gui/internal/steps/AbstractToRepositoryStep.java index 8a72c68bd..ed2eaaa1c 100644 --- a/src/main/java/com/rapidminer/studio/io/gui/internal/steps/AbstractToRepositoryStep.java +++ b/src/main/java/com/rapidminer/studio/io/gui/internal/steps/AbstractToRepositoryStep.java @@ -33,7 +33,6 @@ import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JPanel; -import javax.swing.SwingWorker; import javax.swing.WindowConstants; import javax.swing.event.ChangeListener; @@ -43,6 +42,7 @@ import com.rapidminer.core.io.gui.WizardDirection; import com.rapidminer.core.io.gui.WizardStep; import com.rapidminer.gui.RapidMinerGUI; +import com.rapidminer.gui.tools.MultiSwingWorker; import com.rapidminer.gui.tools.ProgressThread; import com.rapidminer.gui.tools.ProgressThreadDialog; import com.rapidminer.gui.tools.ResourceAction; @@ -76,7 +76,7 @@ public abstract class AbstractToRepositoryStep { + private class ProgressUpdater extends MultiSwingWorker { private static final int IDLE_TIME_MS = 500; @@ -294,8 +294,8 @@ public void viewWillBecomeInvisible(WizardDirection direction) throws InvalidCon // Import data with background worker. backgroundJob.addProgressThreadListener(thread -> backgroundJob = null); - SwingWorker progressUpdater = new ProgressUpdater(); - progressUpdater.execute(); + MultiSwingWorker progressUpdater = new ProgressUpdater(); + progressUpdater.start(); backgroundJob.start(); try { // this call will block the EDT and will continue as soon as diff --git a/src/main/java/com/rapidminer/template/Template.java b/src/main/java/com/rapidminer/template/Template.java index 6df7404c6..f11cd81c1 100644 --- a/src/main/java/com/rapidminer/template/Template.java +++ b/src/main/java/com/rapidminer/template/Template.java @@ -37,7 +37,9 @@ import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.io.process.ProcessOriginProcessXMLFilter; import com.rapidminer.io.process.ProcessOriginProcessXMLFilter.ProcessOriginState; +import com.rapidminer.repository.IOObjectEntry; import com.rapidminer.repository.MalformedRepositoryLocationException; +import com.rapidminer.repository.ProcessEntry; import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; import com.rapidminer.repository.resource.ZipStreamResource; @@ -225,9 +227,9 @@ private void load() throws IOException, RepositoryException { props.load(zip); title = props.getProperty("template.name", NO_TITLE); shortDescription = props.getProperty("template.short_description", NO_DESCRIPTION); - } else if (entryName.endsWith(".rmp")) { + } else if (entryName.endsWith(ProcessEntry.RMP_SUFFIX)) { processName = entryName.split("\\.")[0]; - } else if (entryName.endsWith(".ioo")) { + } else if (entryName.endsWith(IOObjectEntry.IOO_SUFFIX)) { demoData.add(entryName.split("\\.")[0]); } else if ("icon.png".equals(entryName)) { icon = new ImageIcon(Tools.readInputStream(zip)); diff --git a/src/main/java/com/rapidminer/test_utils/Util.java b/src/main/java/com/rapidminer/test_utils/Util.java index 878d517f0..3df849448 100644 --- a/src/main/java/com/rapidminer/test_utils/Util.java +++ b/src/main/java/com/rapidminer/test_utils/Util.java @@ -95,7 +95,7 @@ public static List getExpectedResult(Process process) throws Repositor public static void removeExpectedResults(Process process) throws RepositoryException { Folder folder = process.getRepositoryLocation().locateEntry().getContainingFolder(); - Collection toDelete = new ArrayList(); + Collection toDelete = new ArrayList<>(); for (DataEntry entry : folder.getDataEntries()) { if (entry instanceof IOObjectEntry) { IOObjectEntry ioo = (IOObjectEntry) entry; diff --git a/src/main/java/com/rapidminer/tools/ClassLoaderSwapper.java b/src/main/java/com/rapidminer/tools/ClassLoaderSwapper.java new file mode 100644 index 000000000..d7af9eb32 --- /dev/null +++ b/src/main/java/com/rapidminer/tools/ClassLoaderSwapper.java @@ -0,0 +1,77 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.tools; + +/** + * Helper class to swap the Classloader and then put it back at the end of the try regardless of + * errors. + * + * Using it to clean up code...from: + * + * if (classLoader != null) { - * currentContextClassLoader = + * Thread.currentThread().getContextClassLoader(); - * + * Thread.currentThread().setContextClassLoader(classLoader); - * } try { .... }finally { if + * (currentContextClassLoader != null) { - * + * Thread.currentThread().setContextClassLoader(currentContextClassLoader); - * } } + * + * to try (ClassLoadSwapper sw = ClassLoadSwapper.swapClassLoaderTo(classLoader)) { ... } + * + * @author John Pendzick + * @since 9.3, copied from Radoop extension + */ +public final class ClassLoaderSwapper implements AutoCloseable { + + private final ClassLoader currentCl; + private final ClassLoader newClassLoader; + + public static ClassLoaderSwapper withContextClassLoader(ClassLoader newClassLoader) { + return new ClassLoaderSwapper(newClassLoader); + } + + private ClassLoaderSwapper(ClassLoader newClassLoader) { + + if (newClassLoader == null) { + this.currentCl = null; + this.newClassLoader = null; + } else { + ClassLoader contextCL = Thread.currentThread().getContextClassLoader(); + // already this classloader? nothing to do + if (contextCL == newClassLoader) { + this.currentCl = this.newClassLoader = null; + return; + } + this.currentCl = contextCL; + this.newClassLoader = newClassLoader; + Thread.currentThread().setContextClassLoader(this.newClassLoader); + } + } + + /** Get the class loader that was put as the new context class loader. */ + public ClassLoader getNewClassLoader() { + return this.newClassLoader; + } + + @Override + public void close() { + if (this.currentCl != null) { + Thread.currentThread().setContextClassLoader(this.currentCl); + } + } + +} diff --git a/src/main/java/com/rapidminer/tools/DefaultMailSessionFactory.java b/src/main/java/com/rapidminer/tools/DefaultMailSessionFactory.java index ca5a2b495..535c94226 100644 --- a/src/main/java/com/rapidminer/tools/DefaultMailSessionFactory.java +++ b/src/main/java/com/rapidminer/tools/DefaultMailSessionFactory.java @@ -18,6 +18,7 @@ */ package com.rapidminer.tools; +import java.util.Objects; import java.util.Properties; import java.util.logging.Level; @@ -44,13 +45,13 @@ public class DefaultMailSessionFactory implements MailSessionFactory { /** Check if 256 is allowed */ private static final boolean AES_256_ALLOWED = isAES256Supported(); - /** Elliptic curve Diffie-Hellman Cyphersuites recommended by BSI TR-02102-2 */ + /** Elliptic curve Diffie-Hellman cipher suites recommended by BSI TR-02102-2 */ private static final String ECDH_CIPHERSUITES = "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 " + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 " + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 " + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 "; private static final String ECDH_UNLIMITED_CIPHERSUITES = "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 " + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 "; - /** Diffie-Hellman Cyphersuites recommended by BSI TR-02102-2 */ + /** Diffie-Hellman cipher suites recommended by BSI TR-02102-2 */ private static final String DH_CIPHERSUITES = "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256 " + "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256 " + "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 "; private static final String DH_UNLIMITED_CIPHERSUITES = "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256 " @@ -79,7 +80,7 @@ public Session makeSession() { } else { props.put("mail.from", "no-reply@rapidminer.com"); } - final String user = ParameterService.getParameterValue(RapidMiner.PROPERTY_RAPIDMINER_TOOLS_SMTP_USER); + final String user = Objects.toString(ParameterService.getParameterValue(RapidMiner.PROPERTY_RAPIDMINER_TOOLS_SMTP_USER), ""); props.put("mail.user", user); // Allow debug mode @@ -89,7 +90,7 @@ public Session makeSession() { } // Setup Security - switch (ParameterService.getParameterValue(RapidMiner.PROPERTY_RAPIDMINER_TOOLS_SMTP_SECURITY)) { + switch (Objects.toString(ParameterService.getParameterValue(RapidMiner.PROPERTY_RAPIDMINER_TOOLS_SMTP_SECURITY), "")) { case RapidMiner.PROPERTY_RAPIDMINER_TOOLS_SMTP_SECURITY_STARTTLS: props.setProperty("mail.smtp.starttls.enable", "true"); break; @@ -121,48 +122,49 @@ public Session makeSession() { // Setup Authentication Authenticator authenticator = null; - final String passwd; - try { - if (CipherTools.isKeyAvailable()) { - passwd = CipherTools - .decrypt(ParameterService.getParameterValue(RapidMiner.PROPERTY_RAPIDMINER_TOOLS_SMTP_PASSWD)); - } else { - passwd = ""; + String passwd = Objects.toString(ParameterService.getParameterValue(RapidMiner.PROPERTY_RAPIDMINER_TOOLS_SMTP_PASSWD), ""); + if (CipherTools.isKeyAvailable()) { + try { + passwd = CipherTools.decrypt(passwd); + } catch (CipherException e) { + // passwd is in plaintext LogService.getRoot().log(Level.WARNING, - "com.rapidminer.tools.DefaultMailSessionFactory.smtp_password_cipher_missing"); + "com.rapidminer.tools.DefaultMailSessionFactory.smtp_password_decode_failed"); + } + } else { + LogService.getRoot().log(Level.WARNING, + "com.rapidminer.tools.DefaultMailSessionFactory.smtp_password_cipher_missing"); + } + if (passwd.length() > 0) { + props.setProperty("mail.smtp.submitter", user); + props.setProperty("mail.smtp.auth", "true"); + + // Set the Authentication mechanism + switch (Objects.toString(ParameterService.getParameterValue(RapidMiner.PROPERTY_RAPIDMINER_TOOLS_SMTP_AUTHENTICATION), "")) { + case RapidMiner.PROPERTY_RAPIDMINER_TOOLS_SMTP_AUTHENTICATION_CRAM_MD5: + props.setProperty("mail.smtp.sasl.enable", "true"); + props.setProperty("mail.smtp.sasl.mechanisms", "CRAM-MD5"); + // Workaround for silent sasl downgrade bug in JavaMail < 1.5.2 + props.setProperty("mail.smtp.auth.mechanisms", "DIGEST-MD5"); + break; + case RapidMiner.PROPERTY_RAPIDMINER_TOOLS_SMTP_AUTHENTICATION_NTLM: + props.setProperty("mail.smtp.auth.mechanisms", "NTLM"); + break; + case RapidMiner.PROPERTY_RAPIDMINER_TOOLS_SMTP_AUTHENTICATION_AUTO: + default: + break; } - if (passwd.length() > 0) { - props.setProperty("mail.smtp.submitter", user); - props.setProperty("mail.smtp.auth", "true"); - - // Set the Authentication mechanism - switch (ParameterService.getParameterValue(RapidMiner.PROPERTY_RAPIDMINER_TOOLS_SMTP_AUTHENTICATION)) { - case RapidMiner.PROPERTY_RAPIDMINER_TOOLS_SMTP_AUTHENTICATION_CRAM_MD5: - props.setProperty("mail.smtp.sasl.enable", "true"); - props.setProperty("mail.smtp.sasl.mechanisms", "CRAM-MD5"); - // Workaround for silent sasl downgrade bug in JavaMail < 1.5.2 - props.setProperty("mail.smtp.auth.mechanisms", "DIGEST-MD5"); - break; - case RapidMiner.PROPERTY_RAPIDMINER_TOOLS_SMTP_AUTHENTICATION_NTLM: - props.setProperty("mail.smtp.auth.mechanisms", "NTLM"); - break; - case RapidMiner.PROPERTY_RAPIDMINER_TOOLS_SMTP_AUTHENTICATION_AUTO: - default: - break; - } - authenticator = new Authenticator() { + final String password = passwd; + authenticator = new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(user, passwd); - } - }; - } - } catch (CipherException e) { - LogService.getRoot().log(Level.WARNING, - "com.rapidminer.tools.DefaultMailSessionFactory.smtp_password_decode_failed"); + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(user, password); + } + }; } + String port = ParameterService.getParameterValue(RapidMiner.PROPERTY_RAPIDMINER_TOOLS_SMTP_PORT); if (port != null) { props.setProperty("mail.smtp.port", port); @@ -172,33 +174,33 @@ protected PasswordAuthentication getPasswordAuthentication() { } /** - * Get all PFS Ciphersuites + * Get all PFS cipher suites * - * @return + * @return the supported PFS cipher suites */ private static String getSupportedPFSCipherSuites() { // Tries to follow the BSI TR-02102-2 recommendation // User has to set jdk.tls.ephemeralDHKeySize to 2048 or matched for DH - String cypherSuites = ECDH_CIPHERSUITES; + StringBuilder cypherSuites = new StringBuilder(ECDH_CIPHERSUITES); if (AES_256_ALLOWED) { - cypherSuites += ECDH_UNLIMITED_CIPHERSUITES; + cypherSuites.append(ECDH_UNLIMITED_CIPHERSUITES); } if ("2048".equals(DH_SIZE) || "matched".equals(DH_SIZE)) { - cypherSuites += DH_CIPHERSUITES; + cypherSuites.append(DH_CIPHERSUITES); if (AES_256_ALLOWED) { - cypherSuites += DH_UNLIMITED_CIPHERSUITES; + cypherSuites.append(DH_UNLIMITED_CIPHERSUITES); } } - return cypherSuites; + return cypherSuites.toString(); } /** * Check if AES_256 is allowed * - * @return + * @return {@code true} if AES_256 is allowed */ private static boolean isAES256Supported() { - boolean allowed = false; + boolean allowed; try { allowed = Cipher.getMaxAllowedKeyLength("AES") >= 256; } catch (Exception e) { diff --git a/src/main/java/com/rapidminer/tools/ExtensibleResourceBundle.java b/src/main/java/com/rapidminer/tools/ExtensibleResourceBundle.java index fa3ff1de7..b1b260d9a 100644 --- a/src/main/java/com/rapidminer/tools/ExtensibleResourceBundle.java +++ b/src/main/java/com/rapidminer/tools/ExtensibleResourceBundle.java @@ -151,11 +151,9 @@ public void addResourceBundle(ResourceBundle bundle) { */ public void addResourceBundleAndOverwrite(ResourceBundle bundle) { try { - if (System.getSecurityManager() != null) { - AccessController.checkPermission(new RuntimePermission(PluginSandboxPolicy.RAPIDMINER_INTERNAL_PERMISSION)); - } - } catch (AccessControlException ace) { - LogService.getRoot().log(Level.FINEST, "Internal API, cannot be called by unauthorized sources.", ace); + Tools.requireInternalPermission(); + } catch (UnsupportedOperationException u) { + LogService.getRoot().log(Level.FINEST, u.getMessage(), u.getCause()); addResourceBundle(bundle); return; } diff --git a/src/main/java/com/rapidminer/tools/FileSystemService.java b/src/main/java/com/rapidminer/tools/FileSystemService.java index 5f9a7eeca..743c075a2 100644 --- a/src/main/java/com/rapidminer/tools/FileSystemService.java +++ b/src/main/java/com/rapidminer/tools/FileSystemService.java @@ -33,7 +33,6 @@ * @author Sebastian Land */ public class FileSystemService { - /** folder in which extensions have their workspace */ private static final String RAPIDMINER_EXTENSIONS_FOLDER = "extensions"; /** folder in which extensions get their own folder to work with files */ @@ -44,14 +43,17 @@ public class FileSystemService { private static final String RAPIDMINER_INTERNAL_CACHE = "internal cache"; /** folder which can be used for internal caching of the Global Search feature */ private static final String RAPIDMINER_INTERNAL_CACHE_SEARCH = "search"; - /** folder which is used by BrowserContext for cache data storage */ - private static final String RAPIDMINER_INTERNAL_CACHE_BROWSER = "browser"; + /** folder which is used for the connection file cache */ + private static final String RAPIDMINER_INTERNAL_CACHE_CONNECTION = "connectionFiles"; + /** folder which is used by BrowserContext for cache data storage. Browser cache depends on platform, if you mix DLLs for Win32 and Win64, you get an endless loop */ + private static final String RAPIDMINER_INTERNAL_CACHE_BROWSER = "browser" + (PlatformUtilities.getReleasePlatform() != null ? "-" + PlatformUtilities.getReleasePlatform().name() : ""); /** folder which can be used for internal caching of the content mapper store */ private static final String RAPIDMINER_INTERNAL_CACHE_CONTENT_MAPPER_STORE = "content mapper"; /** folder which can be used as an internal fallback temp folder */ private static final String RAPIDMINER_INTERNAL_CACHE_TEMP = "temp"; + public static final String RAPIDMINER_INTERNAL_CACHE_CONNECTION_FULL = RAPIDMINER_INTERNAL_CACHE + "/" + RAPIDMINER_INTERNAL_CACHE_CONNECTION; public static final String RAPIDMINER_INTERNAL_CACHE_SEARCH_FULL = RAPIDMINER_INTERNAL_CACHE + "/" + RAPIDMINER_INTERNAL_CACHE_SEARCH; public static final String RAPIDMINER_INTERNAL_CACHE_CONTENT_MAPPER_STORE_FULL = RAPIDMINER_INTERNAL_CACHE + "/" + RAPIDMINER_INTERNAL_CACHE_CONTENT_MAPPER_STORE; public static final String RAPIDMINER_INTERNAL_CACHE_BROWSER_FULL = RAPIDMINER_INTERNAL_CACHE + "/" + RAPIDMINER_INTERNAL_CACHE_BROWSER; diff --git a/src/main/java/com/rapidminer/tools/FontTools.java b/src/main/java/com/rapidminer/tools/FontTools.java index ddf523c31..422e54552 100644 --- a/src/main/java/com/rapidminer/tools/FontTools.java +++ b/src/main/java/com/rapidminer/tools/FontTools.java @@ -166,7 +166,7 @@ public static String[] getAvailableFonts() { String[] fonts; try { fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); - } catch (Exception e) { + } catch (Throwable e) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.tools.FontTools.system_font_loading.failed", e); fonts = new String[0]; } diff --git a/src/main/java/com/rapidminer/tools/FunctionWithThrowable.java b/src/main/java/com/rapidminer/tools/FunctionWithThrowable.java new file mode 100644 index 000000000..e20cee16f --- /dev/null +++ b/src/main/java/com/rapidminer/tools/FunctionWithThrowable.java @@ -0,0 +1,103 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.tools; + +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * A wrapper sub-interface of {@link Function} that can handle method references that can also throw an exception. + * By default, the {@link #apply(Object)} method will suppress the exception and return {@code null} on error + * (same as {@link #applyOrNull(Object)}). The opposite is done with {@link #applyOrThrow(Object)}, which will wrap any + * {@link Throwable} in a {@link RuntimeException} if it is not already one and throws that exception. + * + * @param the type of the exception thrown by the function + * + * @since 9.3 + * @author Jan Czogalla + */ +public interface FunctionWithThrowable extends Function { + + /** The basic method; Adds a throws declaration to the method signature. */ + R applyWithException(T t) throws E; + + /** Same as {@link #applyOrNull(Object)} */ + @Override + + default R apply(T t) { + return applyOrNull(t); + } + + /** Applies the function and may return {@code null} on an error */ + @SuppressWarnings("squid:S1181") + default R applyOrNull(T t) { + try { + return applyWithException(t); + } catch (Throwable e) { + return null; + } + } + + /** Applies the function and throws a {@link RuntimeException} if any error occurs. */ + @SuppressWarnings({"squid:S1181", "squid:S00112"}) + default R applyOrThrow(T t) { + try { + return applyWithException(t); + } catch (RuntimeException e) { + throw e; + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + /** Shortcut to wrap a {@link FunctionWithThrowable} to a {@link Function} using {@link #applyOrNull(Object)} */ + static Function suppress(FunctionWithThrowable sf) { + return sf; + } + + /** Shortcut to wrap a {@link FunctionWithThrowable} to a {@link Function} using the given exception handler */ + @SuppressWarnings("squid:S1181") + static Function suppress(FunctionWithThrowable sf, Consumer handler) { + return t -> { + try { + return sf.applyWithException(t); + } catch (Throwable e) { + handler.accept(e); + return null; + } + }; + } + + /** Shortcut to wrap a {@link FunctionWithThrowable} to a {@link Function} using {@link #applyOrThrow(Object)} */ + static Function wrap(FunctionWithThrowable sf) { + return sf::applyOrThrow; + } + + /** Shortcut to wrap a {@link FunctionWithThrowable} to another one using the given exception wrapper */ + @SuppressWarnings("squid:S1181") + static FunctionWithThrowable wrap(FunctionWithThrowable sf, Function wrapper) { + return t -> { + try { + return sf.applyWithException(t); + } catch (Throwable e) { + throw wrapper.apply(e); + } + }; + } +} diff --git a/src/main/java/com/rapidminer/tools/IteratorEnumerationAdapter.java b/src/main/java/com/rapidminer/tools/IteratorEnumerationAdapter.java new file mode 100644 index 000000000..8d2c7a1c1 --- /dev/null +++ b/src/main/java/com/rapidminer/tools/IteratorEnumerationAdapter.java @@ -0,0 +1,127 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.tools; + +import java.util.Enumeration; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * A combination wrapper of {@link Iterator} and {@link Enumeration}. + * + * @author Jan Czogalla + * @since 9.3 + */ +@SuppressWarnings({"squid:S1150", "squid:S2974", "squid:S1610"}) +public abstract class IteratorEnumerationAdapter implements Iterator, Enumeration { + + /** Prevent instantiation outside this class */ + private IteratorEnumerationAdapter(){} + + public static IteratorEnumerationAdapter from(Enumeration enumeration) { + return new EnumerationIterator<>(enumeration); + } + + public static IteratorEnumerationAdapter from(Iterator iterator) { + return new IteratorEnumeration<>(iterator); + } + + /** + * A combination wrapper of {@link Iterator} and {@link Enumeration} for enumerations. + * + * @author Jan Czogalla + * @since 9.3 + */ + private static final class EnumerationIterator extends IteratorEnumerationAdapter { + + private final Enumeration enumeration; + + private EnumerationIterator(Enumeration enumeration) { + this.enumeration = enumeration; + } + + @Override + public boolean hasNext() { + return hasMoreElements(); + } + + @Override + public E next() { + if (!hasMoreElements()) { + throw new NoSuchElementException(); + } + return nextElement(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasMoreElements() { + return enumeration.hasMoreElements(); + } + + @Override + public E nextElement() { + return enumeration.nextElement(); + } + } + + /** + * A combination wrapper of {@link Iterator} and {@link Enumeration} for iterators. + * + * @author Jan Czogalla + * @since 9.3 + */ + private static final class IteratorEnumeration extends IteratorEnumerationAdapter { + + private final Iterator iter; + + private IteratorEnumeration(Iterator iter) { + this.iter = iter; + } + + @Override + public boolean hasNext() { + return iter.hasNext(); + } + + @Override + public E next() { + return iter.next(); + } + + @Override + public void remove() { + iter.remove(); + } + + @Override + public boolean hasMoreElements() { + return hasNext(); + } + + @Override + public E nextElement() { + return next(); + } + } +} diff --git a/src/main/java/com/rapidminer/tools/ListenerTools.java b/src/main/java/com/rapidminer/tools/ListenerTools.java index d575d1314..0bff88d3d 100644 --- a/src/main/java/com/rapidminer/tools/ListenerTools.java +++ b/src/main/java/com/rapidminer/tools/ListenerTools.java @@ -20,6 +20,7 @@ import static com.rapidminer.tools.ConsumerWithThrowable.wrapAndReturn; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; @@ -134,33 +135,19 @@ public static List informAllAndThrow(Collection listeners, Con * the listener type * @return a list of non-{@link OperatorException OperatorExceptions} exceptions that have been thrown by the listeners */ - @SuppressWarnings("squid:S1181") + @SuppressWarnings({"squid:S1181", "unchecked"}) public static List informAllAndThrow(ConsumerWithThrowable initial, Collection listeners, ConsumerWithThrowable method) throws OperatorException { - Throwable initialException = null; - try { - initial.acceptWithException(null); - } catch (Throwable e) { - initialException = e; - if (!(initialException instanceof OperatorException)) { - THROWABLE_LOGGER.accept(initialException); + List newListeners = new ArrayList<>(listeners.size() + 1); + newListeners.add(initial); + listeners.forEach(newListeners::add); + ConsumerWithThrowable newMethod = o -> { + if (o == initial) { + initial.acceptWithException(null); + } else { + method.acceptWithException((L) o); } - } - try { - List throwables = informAllAndThrow(listeners, method); - if (initialException instanceof OperatorException) { - throw (OperatorException) initialException; - } - if (initialException != null) { - throwables.add(0, initialException); - } - return throwables; - } catch (OperatorException e) { - if (initialException instanceof OperatorException) { - initialException.addSuppressed(e); - throw (OperatorException) initialException; - } - throw e; - } + }; + return informAllAndThrow(newListeners, newMethod); } } diff --git a/src/main/java/com/rapidminer/tools/OperatorService.java b/src/main/java/com/rapidminer/tools/OperatorService.java index 1e2465984..e0b2c4547 100644 --- a/src/main/java/com/rapidminer/tools/OperatorService.java +++ b/src/main/java/com/rapidminer/tools/OperatorService.java @@ -428,8 +428,11 @@ private static void parseOperators(GroupTree currentGroup, Element groupElement, String groupKey = currentGroup.getFullyQualifiedKey(); OperatorDescription desc = new OperatorDescription(groupKey, childElement, classLoader, provider, bundle); - desc.setUseExtensionTreeRoot(provider != null ? provider.useExtensionTreeRoot() : false); + desc.setUseExtensionTreeRoot(provider != null && provider.useExtensionTreeRoot()); registerOperator(desc, bundle); + if (desc.getIconName() == null && currentGroup.getIconName() != null) { + desc.setIconName(currentGroup.getIconName()); + } if (desc.getReplacedKeys() != null) { for (String replaces : desc.getReplacedKeys()) { DEPRECATION_MAP.put(replaces, desc.getKey()); diff --git a/src/main/java/com/rapidminer/tools/ParameterService.java b/src/main/java/com/rapidminer/tools/ParameterService.java index c446f2577..26639767b 100644 --- a/src/main/java/com/rapidminer/tools/ParameterService.java +++ b/src/main/java/com/rapidminer/tools/ParameterService.java @@ -409,12 +409,13 @@ public static void saveParameters(File configFile) { for (Entry entry : PARAMETER_MAP.entrySet()) { Parameter parameter = entry.getValue(); String value = parameter.getValue(); + String key = entry.getKey(); // don't store enforced parameters - if (isValueEnforced(entry.getKey())) { - value = ENFORCED_PARAMETER.getOriginalValue(entry.getKey()); + if (isValueEnforced(key)) { + value = ENFORCED_PARAMETER.getOriginalValue(key); } if (value != null) { - properties.put(entry.getKey(), value); + properties.put(key, value); } } BufferedOutputStream out = null; @@ -447,17 +448,7 @@ public static void saveParameters(File configFile) { * without loosing the data. */ public static void registerParameter(ParameterType type) { - // if it is protected, don't allow an overwrite - if (RapidMiner.isParameterProtected(type.getKey())) { - return; - } - Parameter parameter = PARAMETER_MAP.get(type.getKey()); - if (parameter == null) { - parameter = new Parameter(type); - PARAMETER_MAP.put(type.getKey(), parameter); - } else { - parameter.setType(type); - } + registerParameter(type,null, new ParameterScope()); } /** @@ -479,11 +470,13 @@ public static void registerParameter(ParameterType type, String group, Parameter } Parameter parameter = PARAMETER_MAP.get(type.getKey()); if (parameter == null) { - parameter = new Parameter(type, group); + parameter = group == null ? new Parameter(type) : new Parameter(type, group); PARAMETER_MAP.put(type.getKey(), parameter); } else { parameter.setType(type); - parameter.setGroup(group); + if (group != null) { + parameter.setGroup(group); + } } parameter.setScope(scope); } diff --git a/src/main/java/com/rapidminer/tools/ProcessTools.java b/src/main/java/com/rapidminer/tools/ProcessTools.java index ced66c292..bb82b5384 100644 --- a/src/main/java/com/rapidminer/tools/ProcessTools.java +++ b/src/main/java/com/rapidminer/tools/ProcessTools.java @@ -18,7 +18,12 @@ */ package com.rapidminer.tools; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; import com.rapidminer.Process; import com.rapidminer.io.process.ProcessOriginProcessXMLFilter; @@ -27,12 +32,12 @@ import com.rapidminer.operator.OperatorChain; import com.rapidminer.operator.ProcessRootOperator; import com.rapidminer.operator.ProcessSetupError; +import com.rapidminer.operator.UndefinedParameterSetupError; import com.rapidminer.operator.ports.Port; import com.rapidminer.operator.ports.metadata.InputMissingMetaDataError; import com.rapidminer.operator.preprocessing.filter.attributes.SubsetAttributeFilter; import com.rapidminer.operator.tools.AttributeSubsetSelector; import com.rapidminer.parameter.ParameterType; -import com.rapidminer.parameter.ParameterTypeAttribute; import com.rapidminer.repository.Repository; import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; @@ -111,32 +116,11 @@ public static Pair getPortWithoutMandatoryConnection(fi if (process == null) { throw new IllegalArgumentException("process must not be null!"); } - - for (Operator op : process.getAllOperators()) { - // / if operator or one of its parents is disabled, we don't care - if (isSuperOperatorDisabled(op)) { - continue; - } - - // look for matching errors. We can only identify this via metadata errors - for (ProcessSetupError error : op.getErrorList()) { - // the error list of an OperatorChain contains all errors of its children - // we only want errors for the current operator however, so skip otherwise - if (!op.equals(error.getOwner().getOperator())) { - continue; - } - if (error instanceof InputMissingMetaDataError) { - InputMissingMetaDataError err = (InputMissingMetaDataError) error; - // as we don't know what will be sent at runtime, we only look for unconnected - if (!err.getPort().isConnected()) { - return new Pair<>(err.getPort(), err); - } - } - } - } - - // no port with missing input and no connection found - return null; + return process.getAllOperators().stream() + // if operator or one of its parents is disabled, we don't care + .filter(operator -> !isSuperOperatorDisabled(operator)) + .map(ProcessTools::getMissingPortConnection).filter(Objects::nonNull) + .findFirst().orElse(null); } /** @@ -154,17 +138,15 @@ public static Pair getPortWithoutMandatoryConnection(fi * connected; {@code null} otherwise */ public static Pair getMissingPortConnection(Operator operator) { - // look for matching errors. We can only identify this via metadata errors - for (ProcessSetupError error : operator.getErrorList()) { - if (error instanceof InputMissingMetaDataError) { - InputMissingMetaDataError err = (InputMissingMetaDataError) error; + return operator.getErrorList().stream() + // the error list of an OperatorChain contains all errors of its children + // we only want errors for the current operator however, so skip otherwise + .filter(e -> e.getOwner().getOperator() == operator) + // look for matching errors. We can only identify this via metadata errors + .filter(e -> e instanceof InputMissingMetaDataError) // as we don't know what will be sent at runtime, we only look for unconnected - if (!err.getPort().isConnected()) { - return new Pair<>(err.getPort(), err); - } - } - } - return null; + .filter(e -> !((InputMissingMetaDataError) e).getPort().isConnected()) + .findFirst().map(e -> new Pair<>(((InputMissingMetaDataError) e).getPort(), e)).orElse(null); } /** @@ -182,22 +164,7 @@ public static Pair getOperatorWithoutMandatoryParameter if (process == null) { throw new IllegalArgumentException("process must not be null!"); } - - for (Operator op : process.getAllOperators()) { - // if operator or one of its parents is disabled, we don't care - if (isSuperOperatorDisabled(op)) { - continue; - } - - // check all parameters and see if they have no value and are non optional - ParameterType param = getMissingMandatoryParameter(op); - if (param != null) { - return new Pair<>(op, param); - } - } - - // no operator with missing mandatory parameter found - return null; + return getOperatorWithoutMandatoryParameter(process.getAllOperators()); } /** @@ -224,24 +191,36 @@ public static Pair getOperatorWithoutMandatoryParameter // if it has children check them if (operator instanceof OperatorChain) { - for (Operator op : ((OperatorChain) operator).getAllInnerOperators()) { - // if operator or one of its parents is disabled, we don't care - if (isSuperOperatorDisabled(op)) { - continue; - } - - // check all parameters and see if they have no value and are non optional - param = getMissingMandatoryParameter(op); - if (param != null) { - return new Pair<>(op, param); - } - } + return getOperatorWithoutMandatoryParameter(((OperatorChain) operator).getAllInnerOperators()); } // no operator with missing mandatory parameter found return null; } + /** + * Checks whether one of the given operators has a mandatory parameter which + * has no value and no default value. Both the operator and the parameter are then returned. If + * no such operator can be found, returns {@code null}. + * + * @param operators + * the operators in question + * @return the first {@link Operator} found if one of the given operators has a + * mandatory parameter which is neither set nor has a default value; {@code null} otherwise + * @since 9.3 + */ + private static Pair getOperatorWithoutMandatoryParameter(Collection operators) { + return operators.stream() + // if operator or one of its parents is disabled, we don't care + .filter(operator -> !isSuperOperatorDisabled(operator)) + .map(op -> { + // check all parameter related setup errors + ParameterType param = getMissingMandatoryParameter(op); + return param == null ? null : new Pair<>(op, param); + }).filter(Objects::nonNull) + .findFirst().orElse(null); + } + /** * Makes the "subset" parameter of the attribute selector the primary parameter. If the given list does not contain that parameter type, nothing is done. * @@ -303,6 +282,67 @@ public static void setProcessOrigin(Process process) { ProcessOriginProcessXMLFilter.setProcessOriginState(process, origin); } + /** + * Calculates a new name based on the already known names. Will append a number in parenthesis if it is a duplicate. + * + * @param knownNames + * the collection of already known/used names; must not be {@code null} + * @param name + * the name to check; must not be {@code null} + * @return the new name; possibly the same as the input; never {@code null} + * @since 9.3 + */ + public static String getNewName(Collection knownNames, String name) { + if (!knownNames.contains(name)) { + return name; + } + String baseName = name; + int index = baseName.lastIndexOf(" ("); + int i = 2; + if (index >= 0 && baseName.endsWith(")")) { + String suffix = baseName.substring(index + 2, baseName.length() - 1); + try { + i = Integer.parseInt(suffix) + 1; + baseName = baseName.substring(0, index); + if (!knownNames.contains(baseName)) { + return baseName; + } + } catch (NumberFormatException e) { + // not a number; ignore, go with 2 + } + } + String newName; + do { + newName = baseName + " (" + i++ + ')'; + } while (knownNames.contains(newName)); + return newName; + } + + /** + * Calculates new names based on the already known names and returns a map of the renaming for all names that actually changed. + * + * @param knownNames + * the collection of already known/used names; must not be {@code null} + * @param names + * the names to check; must not be {@code null} or contain {@code null} + * @return the new names, mapped from old to new; might be empty; never {@code null} + * @since 9.3 + * @see #getNewName(Collection, String) + */ + public static Map getNewNames(Collection knownNames, Collection names) { + // prevent side effects + knownNames = new HashSet<>(knownNames); + Map nameMap = new LinkedHashMap<>(); + for (String name : names) { + String newName = getNewName(knownNames, name); + if (!name.equals(newName)) { + nameMap.put(name, newName); + } + knownNames.add(newName); + } + return nameMap; + } + /** * Checks whether the given operator has a mandatory parameter which has no value and no default * value and returns the parameter. If no such parameter can be found, returns {@code null}. @@ -313,14 +353,20 @@ public static void setProcessOrigin(Process process) { * {@code null} otherwise */ private static ParameterType getMissingMandatoryParameter(Operator operator) { - for (String key : operator.getParameters().getKeys()) { - ParameterType param = operator.getParameterType(key); - if (!param.isOptional() && (operator.getParameters().getParameterOrNull(key) == null - || param instanceof ParameterTypeAttribute && "".equals(operator.getParameters().getParameterOrNull(key)))) { - return param; - } + List errorList = operator.getErrorList(); + if (errorList.isEmpty()) { + // make sure that setup errors are calculated + operator.checkProperties(); + errorList = operator.getErrorList(); } - return null; + return errorList.stream() + // the error list of an OperatorChain contains all errors of its children + // we only want errors for the current operator however, so skip otherwise + .filter(pse -> pse.getOwner().getOperator() == operator) + // look for matching errors. We can identify this via undefined parameter errors + .filter(pse -> pse instanceof UndefinedParameterSetupError) + .map(pse -> ((UndefinedParameterSetupError) pse).getKey()) + .map(operator::getParameterType).findFirst().orElse(null); } /** diff --git a/src/main/java/com/rapidminer/tools/RMUrlHandler.java b/src/main/java/com/rapidminer/tools/RMUrlHandler.java index e7a4b183f..0307bb13d 100644 --- a/src/main/java/com/rapidminer/tools/RMUrlHandler.java +++ b/src/main/java/com/rapidminer/tools/RMUrlHandler.java @@ -44,6 +44,7 @@ import com.rapidminer.gui.tools.DockingTools; import com.rapidminer.gui.tools.dialogs.ButtonDialog; import com.rapidminer.operator.OperatorCreationException; +import com.rapidminer.repository.ConnectionEntry; import com.rapidminer.repository.Entry; import com.rapidminer.repository.IOObjectEntry; import com.rapidminer.repository.MalformedRepositoryLocationException; @@ -419,6 +420,8 @@ private static void handleRapidMinerURL(String urlStr) { if (RapidMinerGUI.getMainFrame().close()) { OpenAction.open(new RepositoryProcessLocation(location), true); } + } else if (entry instanceof ConnectionEntry) { + OpenAction.showConnectionInformationDialog((ConnectionEntry) entry); } else if (entry instanceof IOObjectEntry) { OpenAction.showAsResult((IOObjectEntry) entry); } else { diff --git a/src/main/java/com/rapidminer/tools/Tools.java b/src/main/java/com/rapidminer/tools/Tools.java index 2300adc52..314c7b13d 100644 --- a/src/main/java/com/rapidminer/tools/Tools.java +++ b/src/main/java/com/rapidminer/tools/Tools.java @@ -18,6 +18,8 @@ */ package com.rapidminer.tools; +import static com.rapidminer.tools.FunctionWithThrowable.suppress; + import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; @@ -38,6 +40,8 @@ import java.nio.channels.FileChannel; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.security.AccessControlException; +import java.security.AccessController; import java.text.DateFormat; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; @@ -59,7 +63,6 @@ import java.util.Map; import java.util.Set; import java.util.TimeZone; -import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.logging.Level; import java.util.regex.Matcher; @@ -94,6 +97,7 @@ import com.rapidminer.repository.Entry; import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; +import com.rapidminer.security.PluginSandboxPolicy; import com.rapidminer.tools.io.Encoding; import com.rapidminer.tools.parameter.ParameterChangeListener; import com.rapidminer.tools.plugin.Plugin; @@ -1350,26 +1354,13 @@ public static void findImplementationsInJar(JarFile jar, Class superClass, Li public static void findImplementationsInJar(ClassLoader loader, JarFile jar, Class superClass, List implementations) { - Enumeration e = jar.entries(); - while (e.hasMoreElements()) { - JarEntry entry = e.nextElement(); - String name = entry.getName(); - int dotClass = name.lastIndexOf(".class"); - if (dotClass < 0) { - continue; - } - name = name.substring(0, dotClass); - name = name.replaceAll("/", "\\."); - try { - Class c = loader.loadClass(name); - if (superClass.isAssignableFrom(c)) { - if (!java.lang.reflect.Modifier.isAbstract(c.getModifiers())) { - implementations.add(name); - } - } - } catch (Throwable t) { - } - } + String classSuffix = ".class"; + int suffixLength = classSuffix.length(); + jar.stream().map(ZipEntry::getName).filter(n -> n.endsWith(classSuffix)) + .map(n -> n.substring(0, n.length() - suffixLength).replace('/', '.')) + .map(suppress(loader::loadClass)) + .filter(c -> c != null && superClass.isAssignableFrom(c) && !java.lang.reflect.Modifier.isAbstract(c.getModifiers())) + .map(Class::getName).forEach(implementations::add); } /** TODO: Looks like this can be replaced by {@link Plugin#getMajorClassLoader()} */ @@ -2261,8 +2252,10 @@ public static boolean canFileBeStoredOnCurrentFilesystem(String fileName) { * Copies the given {@link String} to the system {@link Clipboard}. * * @param s + * the string to copy to the clipboard */ public static void copyStringToClipboard(String s) { + StringSelection stringSelection = new StringSelection(s); Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(stringSelection, null); @@ -2372,11 +2365,13 @@ public static boolean isOperatorInCircle(Operator operator, int maxhops) { for (OutputPort aPort : nextOperator.getOutputPorts().getAllPorts()) { if (aPort.isConnected()) { final Operator anotherOp = aPort.getDestination().getPorts().getOwner().getOperator(); - if (visitedOperators.contains(anotherOp)) { + if (operator == anotherOp) { return true; } - nextConnectedOperators.add(anotherOp); - visitedOperators.add(anotherOp); + if (!visitedOperators.contains(anotherOp)) { + nextConnectedOperators.add(anotherOp); + visitedOperators.add(anotherOp); + } if (--maxAmountVisitedOperators <= 0) { nextConnectedOperators.clear(); visitedOperators.clear(); @@ -2387,4 +2382,21 @@ public static boolean isOperatorInCircle(Operator operator, int maxhops) { } return false; } + + /** + * Checks if the caller has the {@link PluginSandboxPolicy#RAPIDMINER_INTERNAL_PERMISSION} + * + * @throws UnsupportedOperationException + * if the caller is not signed + * @since 9.3 + */ + public static void requireInternalPermission() { + try { + if (System.getSecurityManager() != null) { + AccessController.checkPermission(new RuntimePermission(PluginSandboxPolicy.RAPIDMINER_INTERNAL_PERMISSION)); + } + } catch (AccessControlException e) { + throw new UnsupportedOperationException(I18N.getErrorMessage("access_control.no_internal_permission"), e); + } + } } diff --git a/src/main/java/com/rapidminer/tools/ValidationUtil.java b/src/main/java/com/rapidminer/tools/ValidationUtil.java new file mode 100644 index 000000000..a01e84586 --- /dev/null +++ b/src/main/java/com/rapidminer/tools/ValidationUtil.java @@ -0,0 +1,586 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.tools; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.apache.commons.lang.StringUtils; + +import com.rapidminer.connection.configuration.ConfigurationParameter; + + +/** + * Utility class to check for different argument's validity. Instead of throwing {@link NullPointerException NullPointerExceptions} + * like the {@link Objects} class often does, throws more comprehensible {@link IllegalArgumentException IllegalArgumentExceptions}, + * indicating the arguments name if given. + * + * @author Jan Czogalla + * @since 9.3 + */ +public final class ValidationUtil { + + private static final String DOT_AS_STRING = "."; + + /** Utility class; don't instantiate*/ + private ValidationUtil() {} + + /** + * Checks the given object for {@code null}. Throws a generic {@link IllegalArgumentException} if it is {@code null}, + * otherwise returns the object. + * + * @param o + * the object to test + * @param + * type safety parameter + * @return the object if it is not {@code null} + * @throws IllegalArgumentException + * if the object is {@code null} + * @see #requireNonNull(Object, String) + */ + public static T requireNonNull(T o) { + return requireNonNull(o, null); + } + + /** + * Checks the given object for {@code null}. Throws a customized {@link IllegalArgumentException} if it is {@code null}, + * otherwise returns the object. + * + * @param o + * the object to test + * @param name + * the name of the argument; optional + * @param + * type safety parameter + * @return the object if it is not {@code null} + * @throws IllegalArgumentException + * if the object is {@code null} + */ + public static T requireNonNull(T o, String name) { + if (o == null) { + name = StringUtils.trimToNull(name); + throw new IllegalArgumentException(illegalArgumentMessage("Missing value", name)); + } + return o; + } + + /** + * Checks the given string for {@code null} and emptiness. Throws a generic {@link IllegalArgumentException} + * if it is {@code null} or empty, otherwise returns the string. + * + * @param string + * the string to test + * @return the string if it is not {@code null} or empty + * @throws IllegalArgumentException + * if the string is {@code null} or empty + * @see #requireNonEmptyString(String, String) + */ + public static String requireNonEmptyString(String string) { + return requireNonEmptyString(string, null); + } + + /** + * Checks the given string for {@code null} and emptiness. Throws a customized {@link IllegalArgumentException} + * if it is {@code null} or empty, otherwise returns the string. + * + * @param string + * the string to test + * @param name + * the name of the argument; optional + * @return the string if it is not {@code null} or empty + * @throws IllegalArgumentException + * if the string is {@code null} or empty + * @see #requireNonNull(Object, String) + */ + public static String requireNonEmptyString(String string, String name) { + return requireNonNull(StringUtils.trimToNull(string), name); + } + + /** + * Checks the given string for the dot-character. Throws a customized {@link IllegalArgumentException} if it + * contains a dot, otherwise returns the string. + * + * @param string + * the string to test + * @return the string if it does not contain a {@code .} + * @throws IllegalArgumentException + * if the string contains a dot + */ + public static String requireNoDot(String string) { + return requireNoDot(string, null); + } + + /** + * Checks the given string for the dot-character. Throws a customized {@link IllegalArgumentException} if it + * contains a dot, otherwise returns the string. + * + * @param string + * the string to test + * @param name + * the name of the argument; optional + * @return the string if it does not contain a {@code .} + * @throws IllegalArgumentException + * if the string contains a dot + */ + public static String requireNoDot(String string, String name) { + if (string == null) { + return null; + } + if (string.contains(DOT_AS_STRING)) { + throw new IllegalArgumentException(illegalArgumentMessage("Strings containing dots", name)); + } + return string; + } + + /** + * Checks the given {@link List} for {@code null} and returns a copy of the list with all {@code null} elements removed. + * + * @param list + * the list to test + * @return a copy of the list with only non-{@code null} elements + * @see #stripToEmptyList(List, Predicate) + */ + public static List stripToEmptyList(List list) { + return stripToEmptyList(list, null); + } + + /** + * Checks the given {@link List} for {@code null} and returns a copy of the list with all {@code null}/empty elements removed. + * + * @param list + * the list to test + * @param elementNotEmpty + * predicate to find non-empty elements, can be {@code null} and does not need to be {@code null} sensitive + * @return a copy of the list with only non-{@code null} elements + */ + public static List stripToEmptyList(List list, Predicate elementNotEmpty) { + Predicate acceptable; + if (elementNotEmpty == null) { + acceptable = Objects::nonNull; + } else { + acceptable = o -> Objects.nonNull(o) && elementNotEmpty.test(o); + } + if (list == null) { + return new ArrayList<>(); + } + return list.stream().filter(acceptable).collect(Collectors.toList()); + } + + /** + * Checks the given {@link List list(s)} for duplicates. Uniqueness will be measured by the given {@link Comparator}. + * Throws an {@link IllegalArgumentException} if a duplicate was found; stops at the first found duplicate. + * If no duplicates were found, returns the list. + * + * @param list + * the list to check for duplicates; must not be {@code null} + * @param measure + * the comparator to check uniqueness; optional, if not defined, the natural ordering is used + * @param otherLists + * additional lists to check duplication against; optional, can contain {@code null} entries + * @return the first list if no duplicates were found + * @throws IllegalArgumentException + * if duplicates were found + */ + public static List noDuplicatesAllowed(List list, Comparator measure, List... otherLists) { + return noDuplicatesAllowed(list, measure, null, otherLists); + } + + /** + * Checks the given {@link List list(s)} for duplicates. Uniqueness will be measured by the given {@link Comparator}. + * Throws an {@link IllegalArgumentException} if a duplicate was found; stops at the first found duplicate. + * If no duplicates were found, returns the list. + * + * @param list + * the list to check for duplicates; must not be {@code null} + * @param measure + * the comparator to check uniqueness; optional, if not defined, the natural ordering is used + * @param name + * the name of the argument; optional + * @param otherLists + * additional lists to check duplication against; optional, can contain {@code null} entries + * @return the first list if no duplicates were found + * @throws IllegalArgumentException + * if duplicates were found + */ + @SafeVarargs + public static List noDuplicatesAllowed(List list, Comparator measure, String name, List... otherLists) { + requireNonNull(list, name); + if (list.isEmpty()) { + return list; + } + Set uniqueSet = new TreeSet<>(measure); + name = StringUtils.trimToNull(name); + String errorMessage = illegalArgumentMessage("Duplicates in list", name); + for (T item : list) { + if (!uniqueSet.add(item)) { + throw new IllegalArgumentException(errorMessage + ": " + item); + } + } + if (otherLists == null) { + return list; + } + for (List otherList : otherLists) { + if (otherList == null) { + continue; + } + for (T item : otherList) { + if (!uniqueSet.add(item)) { + throw new IllegalArgumentException(errorMessage + ": " + item); + } + } + } + return list; + } + + /** + * Checks the given {@link List} for {@code null}, only containing {@code null} and emptiness. + * Throws a generic {@link IllegalArgumentException} if it is {@code null}, contains only {@code null} or is empty, + * otherwise returns a copy of the list with all {@code null} elements removed. + * + * @param list + * the list to test + * @return a copy of the list with only non-{@code null} elements + * @throws IllegalArgumentException + * if the list is {@code null}, contains only {@code null} or is empty + * @see #requireNonEmptyList(List, Predicate, String) + */ + public static List requireNonEmptyList(List list) { + return requireNonEmptyList(list, null, null); + } + + /** + * Checks the given {@link List} for {@code null}, only containing {@code null} and emptiness. + * Throws a customized {@link IllegalArgumentException} if it is {@code null}, contains only {@code null} or is empty, + * otherwise returns a copy of the list with all {@code null} elements removed. + * + * @param list + * the list to test + * @param name + * the name of the argument; optional + * @return a copy of the list with only non-{@code null} elements + * @throws IllegalArgumentException + * if the list is {@code null}, contains only {@code null} or is empty + * @see #requireNonEmptyList(List, Predicate, String) + */ + public static List requireNonEmptyList(List list, String name) { + return requireNonEmptyList(list, null, name); + } + + /** + * Checks the given {@link List} for {@code null}, only containing {@code null} or empty elements and emptiness. + * Throws a customized {@link IllegalArgumentException} if it is {@code null}, contains only {@code null} or + * empty elements,or is empty, otherwise returns a copy of the list with all {@code null}/empty elements removed. + * + * @param list + * the list to test + * @param elementNotEmpty + * predicate to find non-empty elements, ca be {@code null} + * @return a copy of the list with only non-{@code null} elements + * @throws IllegalArgumentException + * if the list is {@code null}, contains only {@code null} or empty elements or is empty + * @see #requireNonEmptyList(List, Predicate, String) + */ + public static List requireNonEmptyList(List list, Predicate elementNotEmpty) { + return requireNonEmptyList(list, elementNotEmpty, null); + } + + /** + * Checks the given {@link List} for {@code null}, only containing {@code null} or empty elements and emptiness. + * Throws a customized {@link IllegalArgumentException} if it is {@code null}, contains only {@code null} or + * empty elements,or is empty, otherwise returns a copy of the list with all {@code null}/empty elements removed. + * + * @param list + * the list to test + * @param elementNotEmpty + * predicate to find non-empty elements, ca be {@code null} + * @param name + * the name of the argument; optional + * @return a copy of the list with only non-{@code null} elements + * @throws IllegalArgumentException + * if the list is {@code null}, contains only {@code null} or empty elements or is empty + */ + public static List requireNonEmptyList(List list, Predicate elementNotEmpty, String name) { + requireNonNull(list, name); + list = stripToEmptyList(list, elementNotEmpty); + if (list.isEmpty()) { + name = StringUtils.trimToNull(name); + throw new IllegalArgumentException(illegalArgumentMessage("Empty, null-filled or missing list", name)); + } + return list; + } + + /** + * Same as {@link #dependencySortNoLoops(Function, Collection)}, but instead of throwing an {@link IllegalArgumentException} + * if a circular dependency is found, an empty list is returned. + */ + public static List dependencySortEmptyListForLoops(Function> dependencyExtractor, Collection toSort) { + try { + return dependencySortNoLoops(dependencyExtractor, toSort); + } catch (IllegalArgumentException e) { + return Collections.emptyList(); + } + } + + /** + * Creates a copy of the given collection, whose sort order is specified by a topological sort over the dependencies + * generated by the given extractor. + *
      + * First, the initial dependencies are extracted with the given function and then all transitive dependencies will + * be calculated. If a loop is detected, an {@link IllegalArgumentException} is thrown. + *
      + * Second, all elements from the original collection are sorted by the dependencies, using insertion sort. + * The returned List is a {@link LinkedList}, and changing the list will not necessarily keep the order. + * + * @param dependencyExtractor + * the function to extract the initial dependencies from the original map + * @param toSort + * the collection to be sorted + * @param + * the element type + * @return the sorted list + * @throws IllegalArgumentException + * if a circular dependency is found + * @see #getFullDependencies(Function, Collection) + * @see #sortListByDependency(Collection, Map) + */ + public static List dependencySortNoLoops(Function> dependencyExtractor, Collection toSort) { + Map> fullDependencies = getFullDependencies(dependencyExtractor, toSort); + return sortListByDependency(toSort, fullDependencies); + } + + /** + * Same as {@link #dependencySortNoLoops(BiFunction, Map)}, but instead of throwing an {@link IllegalArgumentException} + * if a circular dependency is found, an empty map is returned. + */ + public static Map dependencySortEmptyMapForLoops(BiFunction> dependencyExtractor, Map toSort) { + try { + return dependencySortNoLoops(dependencyExtractor, toSort); + } catch (IllegalArgumentException e) { + return Collections.emptyMap(); + } + } + + /** + * Creates a copy of the given map, whose sort order is specified by a topological sort over the dependencies + * generated by the given extractor. + *
      + * First, the initial dependencies are extracted with the given function and then all transitive dependencies will + * be calculated. If a loop is detected, an {@link IllegalArgumentException} is thrown. + *
      + * Second, all keys from the original map are sorted by the dependencies, using insertion sort. + *
      + * Lastly, a copy of the original map is created, honoring this new sorting. The sorting is NOT permanent, + * the returned map is a {@link LinkedHashMap}. + * + * @param dependencyExtractor + * the function to extract the initial dependencies from the original map + * @param toSort + * the map to be sorted + * @param + * the key type + * @param + * the value type + * @return the sorted map + * @throws IllegalArgumentException + * if a circular dependency is found + * @see #dependencySortNoLoops(Function, Collection) + */ + public static Map dependencySortNoLoops(BiFunction> dependencyExtractor, Map toSort) { + List sortedKeys = dependencySortNoLoops(k -> dependencyExtractor.apply(k, toSort.get(k)), toSort.keySet()); + return sortedKeys.stream().collect(Collectors.toMap(k -> k, toSort::get, (a, b) -> b, LinkedHashMap::new)); + } + + /** + * Calculates a new name based on the already known names. Will append a number in parenthesis if it is a duplicate. + * If the original name already ends with a number in parenthesis this method will increment the number. + * + * @param knownNames + * A collection that contains the already used names. This method will never generate a name that is already + * inside of this collection. Please note: The newly generated name will not be added to the collection. + * @param name + * The original name. + * @param returnBaseName + * If {@code true} and the original name ends with a number in parenthesis: This method will check whether the + * base name (original name without the number in parenthesis) is unique. If it is unique, then it will return the + * base name. + * @since 9.3 + */ + public static String getNewName(Collection knownNames, String name, boolean returnBaseName) { + if (!knownNames.contains(name)) { + return name; + } + String baseName = name; + int index = baseName.lastIndexOf(" ("); + int i = 2; + if (index >= 0 && baseName.endsWith(")")) { + String suffix = baseName.substring(index + 2, baseName.length() - 1); + try { + i = Integer.parseInt(suffix) + 1; + baseName = baseName.substring(0, index); + if (returnBaseName && !knownNames.contains(baseName)) { + return baseName; + } + } catch (NumberFormatException e) { + // not a number; ignore, go with 2 + } + } + String newName; + do { + newName = baseName + " (" + i++ + ')'; + } while (knownNames.contains(newName)); + return newName; + } + + /** + * Calls {@link ValidationUtil#getNewName(Collection, String, boolean)} with {@code returnBaseName = true} + * + * @since 9.3 + */ + public static String getNewName(Collection knownNames, String name) { + return ValidationUtil.getNewName(knownNames, name, true); + } + + /** + * Checks if the given {@link ConfigurationParameter} is set correctly. + * + * @param parameter + * the parameter, never {@code null} + * @return {@code true} if the parameter is injected or if it has a non-null and non-empty value; {@code false} + * otherwise + */ + public static boolean isValueSet(ConfigurationParameter parameter) { + if (parameter == null) { + throw new IllegalArgumentException("parameter must not be null!"); + } + + return parameter.isInjected() || parameter.getValue() != null && !parameter.getValue().trim().isEmpty(); + } + + + /** + * Merges all provided values into a single array. Will result in an empty array if only null values or + * no values are provided. + * + * @param arraySupplier + * the array supplier + * @param values + * the list of values to merge + * @return the merged list of elements, possibly an empty array, never {@code null} + */ + @SuppressWarnings("unchecked") + public static T[] merge(IntFunction arraySupplier, T[]... values) { + return (values == null ? arraySupplier.apply(0) : + Arrays.stream(values).filter(a -> a != null && a.length > 0) + .flatMap(Arrays::stream).distinct().toArray(arraySupplier)); + } + + /** Create a map of full transitive dependencies. The initial dependencies are created using the given extractor. */ + private static Map> getFullDependencies(Function> dependencyExtractor, Collection extractable) { + Map> dependencies = extractable.stream().collect(Collectors.toMap(k -> k, dependencyExtractor, (a, b) -> b)); + addFullDependencies(dependencies); + return dependencies; + } + + /** + * Completes the given dependency map with all transitive dependencies. Will throw an {@link IllegalArgumentException} + * if circular dependencies are found. + */ + private static void addFullDependencies(Map> dependencies) { + if (dependencies.isEmpty()) { + return; + } + boolean change; + do { + change = false; + dependencies.entrySet().stream().filter(e -> e.getValue().contains(e.getKey())).findAny().ifPresent(k -> { + throw new IllegalArgumentException(illegalArgumentMessage("Circular dependencies", k.toString())); + }); + for (Collection deps : dependencies.values()) { + if (deps.addAll(deps.stream().map(dependencies::get).filter(Objects::nonNull).flatMap(Collection::stream) + .filter(k -> !deps.contains(k)).collect(Collectors.toSet()))) { + change = true; + } + } + } while (change); + } + + /** Sorts the specified collection using the given dependency map and insertion sort. */ + private static List sortListByDependency(Collection keys, Map> fullDependencies) { + if (keys.isEmpty()) { + return new LinkedList<>(); + } + Comparator dependency = getDependencyComparator(fullDependencies); + List sortedKeys = new LinkedList<>(); + for (K key : keys) { + if (sortedKeys.isEmpty()) { + sortedKeys.add(key); + continue; + } + int i = 0; + for (; i < sortedKeys.size(); i++) { + if (dependency.compare(key, sortedKeys.get(i)) < 0) { + break; + } + } + sortedKeys.add(i, key); + } + return sortedKeys; + } + + /** Creates a {@link Comparator} that relies on the given dependency map */ + @SuppressWarnings("unchecked") + private static Comparator getDependencyComparator(Map> fullDependencies) { + return (a, b) -> { + Collection aDependencies = fullDependencies.getOrDefault(a, Collections.emptySet()); + if (aDependencies.contains(b)) { + return 1; + } + Collection bDependencies = fullDependencies.getOrDefault(b, Collections.emptySet()); + if (bDependencies.contains(a)) { + return -1; + } + if (aDependencies.size() != bDependencies.size()) { + return aDependencies.size() - bDependencies.size(); + } + if (a.getClass() == b.getClass() && a instanceof Comparable && b instanceof Comparable) { + return ((Comparable) a).compareTo(b); + } + return 0; + }; + } + + private static String illegalArgumentMessage(String type, String name) { + return type + (name != null ? " for \"" + name + "\"" : "") + " not allowed"; + } +} diff --git a/src/main/java/com/rapidminer/tools/XMLParserException.java b/src/main/java/com/rapidminer/tools/XMLParserException.java index 0dd2ba8c2..ace7d6ff2 100644 --- a/src/main/java/com/rapidminer/tools/XMLParserException.java +++ b/src/main/java/com/rapidminer/tools/XMLParserException.java @@ -1,5 +1,20 @@ /** - * Copyright (C) 2001-2019 RapidMiner GmbH + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.tools; diff --git a/src/main/java/com/rapidminer/tools/config/AbstractConfigurator.java b/src/main/java/com/rapidminer/tools/config/AbstractConfigurator.java index 44323f6ae..e14115dcf 100644 --- a/src/main/java/com/rapidminer/tools/config/AbstractConfigurator.java +++ b/src/main/java/com/rapidminer/tools/config/AbstractConfigurator.java @@ -162,6 +162,7 @@ public final void reregisterCachedParameterHandler(Configurable configurable, St parameterHandlers.put(configurable.getName(), parameterHandlers.remove(oldConfigurableName)); } + @Override public String toString() { return getName(); diff --git a/src/main/java/com/rapidminer/tools/config/Configurable.java b/src/main/java/com/rapidminer/tools/config/Configurable.java index aa135ba67..329f084f7 100644 --- a/src/main/java/com/rapidminer/tools/config/Configurable.java +++ b/src/main/java/com/rapidminer/tools/config/Configurable.java @@ -20,6 +20,8 @@ import java.util.Map; +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.legacy.ConversionException; import com.rapidminer.repository.internal.remote.RemoteRepository; import com.rapidminer.tools.config.gui.ConfigurableDialog; @@ -64,24 +66,24 @@ public interface Configurable { /** Sets the user defined unique name. */ - public void setName(String name); + void setName(String name); /** Gets the user defined unique name. */ - public String getName(); + String getName(); /** * Sets the given parameters. * * @see #getParameters() */ - public void configure(Map parameterValues); + void configure(Map parameterValues); /** * The parameter values representing this Configurable. * * @see #configure(Map) */ - public Map getParameters(); + Map getParameters(); /** * Returns the ID of this configurable in case it was retrieved from RapidMiner Server. This ID @@ -90,14 +92,14 @@ public interface Configurable { * @see #getSource() * @return -1 if this configurable was not loaded from RapidMiner Server */ - public int getId(); + int getId(); /** * Called when loading and creating configurables. * * @see #getId() */ - public void setId(int id); + void setId(int id); /** * If this configurable was loaded from a RapidMiner Server instance, this is the connection it @@ -105,26 +107,26 @@ public interface Configurable { * * @see #getId() */ - public RemoteRepository getSource(); + RemoteRepository getSource(); /** Set when this configurable was loaded from a RapidMiner Server instance. */ - public void setSource(RemoteRepository source); + void setSource(RemoteRepository source); /** * Gets the user defined short info which will be shown in the list on the left */ - public String getShortInfo(); + String getShortInfo(); /** Sets the parameter value for the given key **/ - public void setParameter(String key, String value); + void setParameter(String key, String value); /** Gets the parameter value for the given key **/ - public String getParameter(String key); + String getParameter(String key); /** * Compares the name and the parameter values of this Configurable with a given Configurable **/ - public boolean hasSameValues(Configurable comparedConfigurable); + boolean hasSameValues(Configurable comparedConfigurable); /** * Checks if the Configurable is empty (has no values/only empty values/default values) @@ -134,9 +136,33 @@ public interface Configurable { * @deprecated Use {@link AbstractConfigurable#isEmptyOrDefault(AbstractConfigurator)} instead. **/ @Deprecated - public boolean isEmptyOrDefault(Configurator configurator); + boolean isEmptyOrDefault(Configurator configurator); /** Returns the type id of the corresponding {@link Configurator}. */ - public String getTypeId(); + String getTypeId(); + + /** + * Returns whether this type of {@link Configurable} supports the new {@link ConnectionInformation} management. + * + * @return {@code false} by default + * @since 9.3 + */ + default boolean supportsNewConnectionManagement() { + return false; + } + + /** + * Converts this {@link Configurable} to a {@link ConnectionInformation} if possible. + * + * @throws UnsupportedOperationException + * if this type of {@link Configurable} does not support the new connection management + * @throws ConversionException + * if an error occurred while converting the {@link Configurable} + * @see #supportsNewConnectionManagement() + * @since 9.3 + */ + default ConnectionInformation convert() throws ConversionException { + throw new UnsupportedOperationException("Conversion not available for configurables of type " + getTypeId()); + } } diff --git a/src/main/java/com/rapidminer/tools/config/ConfigurableConnectionHandler.java b/src/main/java/com/rapidminer/tools/config/ConfigurableConnectionHandler.java new file mode 100644 index 000000000..1c72f4fd2 --- /dev/null +++ b/src/main/java/com/rapidminer/tools/config/ConfigurableConnectionHandler.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.tools.config; + +import com.rapidminer.connection.ConnectionHandler; +import com.rapidminer.connection.legacy.ConversionService; + + +/** + * {@link ConnectionHandler} which can convert existing {@link Configurable Configurables} into {@link com.rapidminer.connection.ConnectionInformation ConnectionInformations} + * + * @since 9.3 + * @author Jonas Wilms-Pfau + */ +public interface ConfigurableConnectionHandler extends ConnectionHandler, ConversionService { + +} diff --git a/src/main/java/com/rapidminer/tools/config/gui/ConfigurableAdminPasswordDialog.java b/src/main/java/com/rapidminer/tools/config/gui/ConfigurableAdminPasswordDialog.java index df57e7332..95a769eab 100644 --- a/src/main/java/com/rapidminer/tools/config/gui/ConfigurableAdminPasswordDialog.java +++ b/src/main/java/com/rapidminer/tools/config/gui/ConfigurableAdminPasswordDialog.java @@ -41,6 +41,7 @@ import com.rapidminer.tools.I18N; import com.rapidminer.tools.config.ConfigurationManager; +import static com.rapidminer.repository.internal.remote.RemoteRepository.AuthenticationType.BASIC; /** * Dialog asking for admin password of a given {@link RemoteRepository}. @@ -127,7 +128,7 @@ private void checkConnection() { public void run() { RemoteRepositoryFactory remoteRepositoryFactory = RemoteRepositoryFactoryRegistry.INSTANCE.get(); final String error = remoteRepositoryFactory != null - ? remoteRepositoryFactory.checkConfiguration(sourceName, repositoryURL, getUserName(), getPassword()) + ? remoteRepositoryFactory.checkConfiguration(sourceName, repositoryURL, getUserName(), getPassword(), BASIC) : I18N.getGUILabel("error.configurable_dialog.remote_repo_factory_not_available"); SwingUtilities.invokeLater(new Runnable() { diff --git a/src/main/java/com/rapidminer/tools/config/gui/ConfigurableDialog.java b/src/main/java/com/rapidminer/tools/config/gui/ConfigurableDialog.java index cee516921..ae5cd7166 100644 --- a/src/main/java/com/rapidminer/tools/config/gui/ConfigurableDialog.java +++ b/src/main/java/com/rapidminer/tools/config/gui/ConfigurableDialog.java @@ -67,6 +67,10 @@ import org.jdesktop.swingx.painter.MattePainter; import com.rapidminer.Process; +import com.rapidminer.connection.adapter.ConnectionAdapter; +import com.rapidminer.connection.adapter.ConnectionAdapterHandler; +import com.rapidminer.connection.gui.ConnectionCreationDialog; +import com.rapidminer.connection.gui.components.DeprecationWarning; import com.rapidminer.gui.ApplicationFrame; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.tools.ExtendedJScrollPane; @@ -83,10 +87,12 @@ import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryManager; import com.rapidminer.repository.internal.remote.RemoteRepository; +import com.rapidminer.repository.local.LocalRepository; import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; import com.rapidminer.tools.config.AbstractConfigurable; import com.rapidminer.tools.config.Configurable; +import com.rapidminer.tools.config.ConfigurationException; import com.rapidminer.tools.config.ConfigurationManager; import com.rapidminer.tools.config.actions.ActionResult; import com.rapidminer.tools.config.actions.ActionResult.Result; @@ -205,6 +211,16 @@ public class ConfigurableDialog extends ButtonDialog { /** the button which can be clicked to perform the test action for a configurable */ private JButton testButton; + /** + * The button that triggers the conversion to a new + * {@link com.rapidminer.connection.ConnectionInformation ConnectionInformation} if possible + * + * @see Configurable#supportsNewConnectionManagement() + * @see Configurable#convert() + * @since 9.3 + */ + private JButton convertButton; + /** the outer panel of the parameters part */ private JPanel outerPanel; @@ -688,6 +704,7 @@ private void initGUI() { realOuterPanel.add(pagePanel, BorderLayout.CENTER); layoutDefault(outerLayer, makeSaveButton(), makeCancel()); + new DeprecationWarning("manage_configurables").addToDialog(this); setDefaultSize(ButtonDialog.HUGE); setLocationRelativeTo(ApplicationFrame.getApplicationFrame()); setModal(true); @@ -1247,21 +1264,66 @@ public void mouseEntered(MouseEvent e) { gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.NONE; - configActionsButton = new JButton(new ResourceActionAdapter(false, "configurable_dialog.show_actions") { + convertButton = new JButton(new ResourceAction(false, "configurable_dialog.convert_configurable") { + + @Override + protected void loggedActionPerformed(ActionEvent e) { + // skip if we started this without configurable or is unconvertable + Configurable selectedConfig = getSelectedValue(); + if (selectedConfig == null || !selectedConfig.supportsNewConnectionManagement()) { + return; + } + ConnectionAdapterHandler handler = ConnectionAdapterHandler.getHandler(selectedConfig.getTypeId()); + if (handler == null) { + return; + } + // create copy and set current values + Configurable configurable; + try { + configurable = handler.create(selectedConfig.getName(), selectedConfig.getParameters()); + } catch (ConfigurationException ce) { + SwingTools.showSimpleErrorMessage(ConfigurableDialog.this, + "configuration.dialog.general", ce, ce.getMessage()); + return; + } + localController.saveConfigurable(configurable, configParamPanel.getParameters()); + // prevent multiple invocations + setEnabled(false); + Repository repository = selectedConfig.getSource(); + if (repository == null) { + // no repo => local repo; preselect first local repo + repository = RepositoryManager.getInstance(null).getRepositories().stream() + .filter(r -> r instanceof LocalRepository).findFirst().orElse(null); + } + ConfigurableDialog parent = ConfigurableDialog.this; + ConnectionCreationDialog conversionDialog = new ConnectionCreationDialog(parent, repository); + String type = handler.getType(); + conversionDialog.preFill(type, configurable.getName()); + conversionDialog.setConverter(configurable::convert); + conversionDialog.setVisible(true); + setEnabled(true); + } + }); + convertButton.setEnabled(false); + actionPanel.add(convertButton, gbc); + + gbc.gridx++; + configActionsButton = new JButton(new ResourceAction(false, "configurable_dialog.show_actions") { private static final long serialVersionUID = 1L; @Override public void loggedActionPerformed(ActionEvent e) { // skip if we started this without configurable - if (getSelectedValue() == null) { + Configurable selectedValue = getSelectedValue(); + if (selectedValue == null) { return; } Configurable configValue = previousConfigurable; if (AbstractConfigurable.class.isAssignableFrom(configValue.getClass())) { AbstractConfigurable configurable = (AbstractConfigurable) configValue; - if (configurable.getActions() != null && configurable.getActions().size() > 0) { + if (configurable.getActions() != null && !configurable.getActions().isEmpty()) { JPopupMenu actionMenu = new ScrollableJPopupMenu(); // create one menu item for each action defined by the configurable for (final ConfigurableAction action : configurable.getActions()) { @@ -1269,25 +1331,21 @@ public void loggedActionPerformed(ActionEvent e) { actionItem.setIcon(SwingTools.createIcon("24/" + action.getIconName())); actionItem.setText(action.getName()); actionItem.setToolTipText(action.getTooltip()); - actionItem.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - // reset last results - testLabel.setIcon(null); - - // store fresh changes - previousConfigurable = getSelectedValue(); - if (previousConfigurable.getSource() == null) { - localController.saveConfigurable(previousConfigurable, - configParamPanel.getParameters()); - localController.executeConfigurableAction(action); - } else { - remoteControllers.get(previousConfigurable.getSource().getName()) - .saveConfigurable(previousConfigurable, configParamPanel.getParameters()); - remoteControllers.get(previousConfigurable.getSource().getName()) - .executeConfigurableAction(action); - } + actionItem.addActionListener(event -> { + // reset last results + testLabel.setIcon(null); + + // store fresh changes + previousConfigurable = selectedValue; + if (previousConfigurable.getSource() == null) { + localController.saveConfigurable(previousConfigurable, + configParamPanel.getParameters()); + localController.executeConfigurableAction(action); + } else { + remoteControllers.get(previousConfigurable.getSource().getName()) + .saveConfigurable(previousConfigurable, configParamPanel.getParameters()); + remoteControllers.get(previousConfigurable.getSource().getName()) + .executeConfigurableAction(action); } }); @@ -1305,14 +1363,15 @@ public void actionPerformed(ActionEvent e) { gbc.gridx += 1; gbc.weightx = 0.0; - testButton = new JButton(new ResourceActionAdapter(false, "configurable_dialog.test_configurable") { + testButton = new JButton(new ResourceAction(false, "configurable_dialog.test_configurable") { private static final long serialVersionUID = 1L; @Override public void loggedActionPerformed(ActionEvent e) { // skip if we started this without configurable - if (getSelectedValue() == null) { + Configurable selectedValue = getSelectedValue(); + if (selectedValue == null) { return; } @@ -1323,7 +1382,7 @@ public void loggedActionPerformed(ActionEvent e) { testLabel.setIcon(null); // store fresh changes - previousConfigurable = getSelectedValue(); + previousConfigurable = selectedValue; if (previousConfigurable.getSource() == null) { localController.saveConfigurable(previousConfigurable, configParamPanel.getParameters()); } else { @@ -1680,11 +1739,13 @@ private void updateButtonState(boolean resetMessage) { nameLabel.setIcon(SwingTools.createIcon("24/" + ConfigurationManager.getInstance().getAbstractConfigurator(configurable.getTypeId()).getIconName())); - if (configurable != null && AbstractConfigurable.class.isAssignableFrom(configurable.getClass())) { + if (AbstractConfigurable.class.isAssignableFrom(configurable.getClass())) { AbstractConfigurable abstractConfig = (AbstractConfigurable) configurable; configActionsButton.setEnabled(abstractConfig.getActions() != null); testButton.setEnabled(abstractConfig.getTestAction() != null); } + convertButton.setEnabled(configurable.supportsNewConnectionManagement() + && ConnectionAdapterHandler.getHandler(configurable.getTypeId()) != null); } updateRefreshButton(); diff --git a/src/main/java/com/rapidminer/tools/config/jwt/JwtReader.java b/src/main/java/com/rapidminer/tools/config/jwt/JwtReader.java index 35d89be12..47ce978c5 100644 --- a/src/main/java/com/rapidminer/tools/config/jwt/JwtReader.java +++ b/src/main/java/com/rapidminer/tools/config/jwt/JwtReader.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.InputStream; +import java.net.HttpURLConnection; import java.net.URLConnection; import java.util.Base64; @@ -33,10 +34,32 @@ /** * Retrieves a {@link JwtClaim} from a {@link RemoteRepository}, which contains additional information about the user * - * @since 8.1.0 * @author Jonas Wilms-Pfau + * @since 8.1.0 */ public class JwtReader { + + /** + * JWT Wrapper object + * + * @author Jonas Wilms-Pfau + * @since 8.1.0 + */ + @JsonIgnoreProperties(ignoreUnknown = true) + private static class JwtWrapper { + + private String idToken; + + String getIdToken() { + return idToken; + } + + public void setIdToken(String idToken) { + this.idToken = idToken; + } + + } + /** * JWT Specification */ @@ -52,32 +75,34 @@ public class JwtReader { * Location of the tokenservice */ private static final String TOKENSERVICE_RELATIVE_URL = "internal/jaxrest/tokenservice"; + /** + * Authorization header key for a connection + */ + private static final String AUTH = "Authorization"; + /** + * Bearer for the authorization header + */ + private static final String BEARER = "Bearer "; + private ObjectMapper mapper = new ObjectMapper(); /** * Read the claim from the remote token service without verifying the signature * *

      - * Warning: Don't use the result of this method to give access to sensitive information! + * Warning: Don't use the result of this method to give access to sensitive information! *

      * * @return JwtClaim or null - * * @throws RepositoryException * @throws IOException */ public JwtClaim readClaim(RemoteRepository source) throws RepositoryException, IOException { - if (source == null) { - return null; - } - URLConnection connection = source.getHTTPConnection(TOKENSERVICE_RELATIVE_URL, true); - if (connection == null) { - throw new RepositoryException("Could not connect to TokenService."); - } - ObjectMapper mapper = new ObjectMapper(); - try (InputStream inputStream = connection.getInputStream()) { - //First extract the outer wrapper - JwtWrapper wrapper = mapper.readValue(inputStream, JwtWrapper.class); + try { + JwtWrapper wrapper = loadJwtWrapper(source); + if (wrapper == null) { + return null; + } //Split the token into header, payload and signature String[] token = wrapper.getIdToken().split(JWT_SEPARATOR_REGEX); //Verify the structure of the Token @@ -96,23 +121,41 @@ public JwtClaim readClaim(RemoteRepository source) throws RepositoryException, I } } - /*** - * JWT Wrapper object + /** + * Retrieve the JWT token from remote and set it on the given connection as authorization header. * - * @since 8.1.0 - * @author Jonas Wilms-Pfau + * @param repository + * the {@link RemoteRepository} that should be accessed + * @param connection + * the connection to add an authorization header to + * @throws IOException + * @throws RepositoryException + * @since 9.3 */ - @JsonIgnoreProperties(ignoreUnknown = true) - private static class JwtWrapper { - - private String idToken; + public void setJwtAuthorization(RemoteRepository repository, HttpURLConnection connection) throws IOException, RepositoryException { + final JwtWrapper jwtWrapper = loadJwtWrapper(repository); + if (jwtWrapper == null) { + return; + } + String token = jwtWrapper.getIdToken(); + connection.setRequestProperty(AUTH, BEARER + token); + } - public String getIdToken() { - return idToken; + /** + * Load the JwtWrapper containing idToken and expiration date from a source repository. + */ + private JwtWrapper loadJwtWrapper(RemoteRepository source) throws IOException, RepositoryException { + if (source == null) { + return null; + } + URLConnection connection = source.getHTTPConnection(TOKENSERVICE_RELATIVE_URL, true); + if (connection == null) { + throw new RepositoryException("Could not connect to TokenService."); } - public void setIdToken(String idToken) { - this.idToken = idToken; + try (InputStream inputStream = connection.getInputStream()) { + //First extract the outer wrapper + return mapper.readValue(inputStream, JwtWrapper.class); } } } diff --git a/src/main/java/com/rapidminer/tools/container/Pair.java b/src/main/java/com/rapidminer/tools/container/Pair.java index 86fc2cf22..c4aee4baa 100644 --- a/src/main/java/com/rapidminer/tools/container/Pair.java +++ b/src/main/java/com/rapidminer/tools/container/Pair.java @@ -19,6 +19,7 @@ package com.rapidminer.tools.container; import java.io.Serializable; +import java.util.Objects; /** @@ -57,18 +58,12 @@ public void setSecond(K second) { @Override public String toString() { - String tString = (getFirst() == null) ? "null" : getFirst().toString(); - String kString = (getSecond() == null) ? "null" : getSecond().toString(); - return tString + " : " + kString; + return first + " : " + second; } @Override public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((first == null) ? 0 : first.hashCode()); - result = prime * result + ((second == null) ? 0 : second.hashCode()); - return result; + return Objects.hash(first, second); } @Override @@ -80,20 +75,6 @@ public boolean equals(Object obj) { return false; } Pair other = (Pair) obj; - if (first == null) { - if (other.first != null) { - return false; - } - } else if (!first.equals(other.first)) { - return false; - } - if (second == null) { - if (other.second != null) { - return false; - } - } else if (!second.equals(other.second)) { - return false; - } - return true; + return Objects.equals(first, other.first) && Objects.equals(second, other.second); } } diff --git a/src/main/java/com/rapidminer/tools/expression/ExpressionParserBuilder.java b/src/main/java/com/rapidminer/tools/expression/ExpressionParserBuilder.java index 51404c8e9..5fb76297d 100644 --- a/src/main/java/com/rapidminer/tools/expression/ExpressionParserBuilder.java +++ b/src/main/java/com/rapidminer/tools/expression/ExpressionParserBuilder.java @@ -26,6 +26,7 @@ import com.rapidminer.tools.expression.internal.ConstantResolver; import com.rapidminer.tools.expression.internal.SimpleExpressionContext; import com.rapidminer.tools.expression.internal.antlr.AntlrParser; +import com.rapidminer.tools.expression.internal.function.eval.AttributeEvaluation; import com.rapidminer.tools.expression.internal.function.eval.Evaluation; import com.rapidminer.tools.expression.internal.function.eval.TypeConstants; import com.rapidminer.tools.expression.internal.function.process.MacroValue; @@ -78,6 +79,9 @@ public ExpressionParser build() { evalFunction = new Evaluation(); functions.add(evalFunction); } + //add the attribute eval function + AttributeEvaluation attributeEvalFunction = new AttributeEvaluation(); + functions.add(attributeEvalFunction); // add eval constants constantResolvers.add(new ConstantResolver(TypeConstants.INSTANCE.getKey(), TypeConstants.INSTANCE.getConstants())); @@ -90,6 +94,7 @@ public ExpressionParser build() { // set parser for eval function evalFunction.setParser(parser); } + attributeEvalFunction.setContext(context); return parser; } diff --git a/src/main/java/com/rapidminer/tools/expression/internal/SimpleExpressionEvaluator.java b/src/main/java/com/rapidminer/tools/expression/internal/SimpleExpressionEvaluator.java index b10d50a2a..455328762 100644 --- a/src/main/java/com/rapidminer/tools/expression/internal/SimpleExpressionEvaluator.java +++ b/src/main/java/com/rapidminer/tools/expression/internal/SimpleExpressionEvaluator.java @@ -184,47 +184,11 @@ public SimpleExpressionEvaluator(Date dateValue, ExpressionType type) { } private static DoubleCallable makeConstantCallable(final double doubleValue) { - return new DoubleCallable() { - - @Override - public double call() { - return doubleValue; - } - - }; - } - - private static Callable makeConstantCallable(final Boolean booleanValue) { - return new Callable() { - - @Override - public Boolean call() { - return booleanValue; - } - - }; + return () -> doubleValue; } - private static Callable makeConstantCallable(final String stringValue) { - return new Callable() { - - @Override - public String call() { - return stringValue; - } - - }; - } - - private static Callable makeConstantCallable(final Date dateValue) { - return new Callable() { - - @Override - public Date call() { - return dateValue; - } - - }; + private static Callable makeConstantCallable(V value) { + return () -> value; } @Override diff --git a/src/main/java/com/rapidminer/tools/expression/internal/function/AbstractArbitraryStringInputStringOutputFunction.java b/src/main/java/com/rapidminer/tools/expression/internal/function/AbstractArbitraryStringInputStringOutputFunction.java index 518503d15..3bcf9aa80 100644 --- a/src/main/java/com/rapidminer/tools/expression/internal/function/AbstractArbitraryStringInputStringOutputFunction.java +++ b/src/main/java/com/rapidminer/tools/expression/internal/function/AbstractArbitraryStringInputStringOutputFunction.java @@ -87,8 +87,8 @@ public ExpressionEvaluator compute(ExpressionEvaluator... inputEvaluators) { */ protected Callable makeStringCallable(final ExpressionEvaluator[] inputEvaluators) { final int inputLength = inputEvaluators.length; - final String[] constantValues = new String[inputLength]; try { + final String[] constantValues = new String[inputLength]; int i = 0; // evaluate which expressions are constant for (ExpressionEvaluator exp : inputEvaluators) { @@ -100,30 +100,18 @@ protected Callable makeStringCallable(final ExpressionEvaluator[] inputE if (isResultConstant(inputEvaluators)) { // compute the result only once and re-use this final String result = compute(constantValues); - return new Callable() { + return () -> result; - @Override - public String call() throws Exception { - return result; - } - }; - - } else { - final String[] values = new String[inputLength]; - - return new Callable() { - - @Override - public String call() throws Exception { - // collect the constant values and fetch the not constant values - for (int j = 0; j < inputLength; j++) { - values[j] = inputEvaluators[j].isConstant() ? constantValues[j] : inputEvaluators[j] - .getStringFunction().call(); - } - return compute(values); - } - }; } + return () -> { + final String[] values = new String[inputLength]; + // collect the constant values and fetch the not constant values + for (int j = 0; j < inputLength; j++) { + values[j] = inputEvaluators[j].isConstant() ? constantValues[j] : inputEvaluators[j] + .getStringFunction().call(); + } + return compute(values); + }; } catch (ExpressionParsingException e) { throw e; } catch (Exception e) { diff --git a/src/main/java/com/rapidminer/tools/expression/internal/function/AbstractFunction.java b/src/main/java/com/rapidminer/tools/expression/internal/function/AbstractFunction.java index 90ddb038b..2f48faeb8 100644 --- a/src/main/java/com/rapidminer/tools/expression/internal/function/AbstractFunction.java +++ b/src/main/java/com/rapidminer/tools/expression/internal/function/AbstractFunction.java @@ -111,11 +111,15 @@ protected ExpressionType getResultType(ExpressionEvaluator... inputEvaluators) { * @return {@code true} if the result of this function is constant */ protected boolean isResultConstant(ExpressionEvaluator... inputEvaluators) { - boolean isConstant = isConstantOnConstantInput(); - for (ExpressionEvaluator input : inputEvaluators) { - isConstant = isConstant && input.isConstant(); + if (!isConstantOnConstantInput()) { + return false; } - return isConstant; + for (ExpressionEvaluator inputEvaluator : inputEvaluators) { + if (!inputEvaluator.isConstant()) { + return false; + } + } + return true; } } diff --git a/src/main/java/com/rapidminer/tools/expression/internal/function/eval/AbstractEvaluation.java b/src/main/java/com/rapidminer/tools/expression/internal/function/eval/AbstractEvaluation.java new file mode 100644 index 000000000..8970bd8e9 --- /dev/null +++ b/src/main/java/com/rapidminer/tools/expression/internal/function/eval/AbstractEvaluation.java @@ -0,0 +1,355 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General + * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses/. + */ +package com.rapidminer.tools.expression.internal.function.eval; + +import java.util.Date; +import java.util.concurrent.Callable; + +import com.rapidminer.tools.Ontology; +import com.rapidminer.tools.expression.DoubleCallable; +import com.rapidminer.tools.expression.ExpressionEvaluator; +import com.rapidminer.tools.expression.ExpressionParsingException; +import com.rapidminer.tools.expression.ExpressionType; +import com.rapidminer.tools.expression.Function; +import com.rapidminer.tools.expression.FunctionDescription; +import com.rapidminer.tools.expression.FunctionInputException; +import com.rapidminer.tools.expression.internal.SimpleExpressionEvaluator; +import com.rapidminer.tools.expression.internal.antlr.AntlrParser; +import com.rapidminer.tools.expression.internal.function.AbstractFunction; + + +/** + * Abstract superclass of {@link Evaluation} and {@link AttributeEvaluation}. + * A {@link Function} that evaluates subexpressions using an {@link AntlrParser} or evaluates an attribute specified + * by the subexpression. Requires a second parameter that specifies the type in case of non-constant input. + * + * @author Gisa Schaefer + * + */ +abstract class AbstractEvaluation extends AbstractFunction { + + + AbstractEvaluation(String i18nKey) { + super(i18nKey, FunctionDescription.UNFIXED_NUMBER_OF_ARGUMENTS, Ontology.ATTRIBUTE_VALUE); + } + + /** + * Checks if the computation can take place, called at the beginning of {@link #compute(ExpressionEvaluator...)}. + */ + protected abstract void checkSetup(); + + @Override + public ExpressionEvaluator compute(final ExpressionEvaluator... inputEvaluators) { + checkSetup(); + // check for string inputs + getResultType(inputEvaluators); + + // Note that if inputEvaluators[0] is constant then this does not mean the result of eval is + // constant, it only means that the return type is constant. Example: 'eval("attribute_1")' + // has the constant string input "attribute_1", but will result in a non-constant evaluator + // that returns the different values of the attribute. + + if (inputEvaluators.length == 1) { + + if (!inputEvaluators[0].isConstant()) { + throw new FunctionInputException("expression_parser.eval.non_constant_single_argument", + getFunctionName()); + } + // if eval has one constant argument compute it once using the parser + return compute(inputEvaluators[0]); + + } else if (inputEvaluators.length == 2) { + + if (inputEvaluators[0].isConstant()) { + // if eval has one constant argument and one type argument compute result of first + // argument once using the parser and check the type + return computeAndCheckType(inputEvaluators[0], getExpectedReturnType(inputEvaluators[1])); + } else { + // if eval has one non-constant argument and one type argument create callables + // depending on the type that do the same in the case above + final ExpressionType expectedType = getExpectedReturnType(inputEvaluators[1]); + switch (expectedType) { + case DATE: + return makeDateEvaluator(expectedType, inputEvaluators[0]); + case DOUBLE: + case INTEGER: + return makeDoubleEvaluator(expectedType, inputEvaluators[0]); + case BOOLEAN: + return makeBooleanEvaluator(expectedType, inputEvaluators[0]); + case STRING: + default: + return makeStringEvaluator(expectedType, inputEvaluators[0]); + } + } + + } else { + throw new FunctionInputException("expression_parser.function_wrong_input_two", getFunctionName(), 1, 2, + inputEvaluators.length); + } + + } + + @Override + protected ExpressionType computeType(ExpressionType... inputTypes) { + // check if inputs are strings, otherwise throw exception + for (ExpressionType type : inputTypes) { + if ((type != ExpressionType.STRING)) { + throw new FunctionInputException("expression_parser.function_wrong_type", getFunctionName(), + "nominal"); + } + } + // is not used to compute the resultType, only used to check the input types + return null; + } + + /** + * Evaluates the expression into a String and feeds it to the parser. + * + * @param subexpressionEvaluator + * the evaluator whose string function call yields the expression string + * @return the result of parsing the expression string + */ + private ExpressionEvaluator compute(ExpressionEvaluator subexpressionEvaluator) { + String expressionString = null; + try { + expressionString = subexpressionEvaluator.getStringFunction().call(); + } catch (ExpressionParsingException e) { + throw e; + } catch (Exception e) { + throw new ExpressionParsingException(e); + } + + return compute(expressionString); + } + + /** + * Computes an expression evaluator from the inner expression string. + * + * @param expressionString + * the function input, can be {@code null} + * @return the evaluator + */ + protected abstract ExpressionEvaluator compute(String expressionString); + + /** + * Evaluates the expression into a String, feeds it to the parser and checks if the resulting + * type is the expected type. If the resulting type is not as expected and the expected type is + * String then converts the result to a string evaluator. If the expected type is double and the + * result type is integer the result type is changed to double. + * + * @param subexpressionEvaluator + * the evaluator whose string function call yields the expression string + * @param expectedType + * the expected type of the result + * @return the result of parsing the expression string + */ + private ExpressionEvaluator computeAndCheckType(ExpressionEvaluator subexpressionEvaluator, + ExpressionType expectedType) { + ExpressionEvaluator outEvaluator = compute(subexpressionEvaluator); + if (outEvaluator.getType() == expectedType) { + return outEvaluator; + } else if (expectedType == ExpressionType.DOUBLE && outEvaluator.getType() == ExpressionType.INTEGER) { + // use same resulting evaluator but with different type + return new SimpleExpressionEvaluator(outEvaluator.getDoubleFunction(), expectedType, + outEvaluator.isConstant()); + } else if (expectedType == ExpressionType.STRING) { + return convertToStringEvaluator(outEvaluator); + } else { + throw new FunctionInputException("expression_parser.eval.type_not_matching", getFunctionName(), + getConstantName(outEvaluator.getType()), getConstantName(expectedType)); + } + } + + /** + * Converts the outEvaluator into an {@link ExpressionEvaluator} of type string. If outEvaluator + * is constant the result is also constant. + * + * @param outEvaluator + * a evaluator which is not of type String + * @return an {@link ExpressionEvaluator} of type String + */ + private ExpressionEvaluator convertToStringEvaluator(final ExpressionEvaluator outEvaluator) { + if (outEvaluator.isConstant()) { + try { + String stringValue = getStringValue(outEvaluator); + return new SimpleExpressionEvaluator(stringValue, ExpressionType.STRING); + } catch (ExpressionParsingException e) { + throw e; + } catch (Exception e) { + throw new ExpressionParsingException(e); + } + } else { + Callable stringCallable = () -> getStringValue(outEvaluator); + return new SimpleExpressionEvaluator(stringCallable, ExpressionType.STRING, false); + } + } + + /** + * Calculates the String value that the outEvaluator should return. + */ + private String getStringValue(ExpressionEvaluator outEvaluator) throws Exception { + switch (outEvaluator.getType()) { + case DOUBLE: + case INTEGER: + return doubleToString(outEvaluator.getDoubleFunction().call(), + outEvaluator.getType() == ExpressionType.INTEGER); + case BOOLEAN: + return booleanToString(outEvaluator.getBooleanFunction().call()); + case DATE: + return dateToString(outEvaluator.getDateFunction().call()); + default: + // cannot happen + return null; + } + } + + /** + * Converts the input to a string with special missing value handling + */ + private String dateToString(Date input) { + if (input == null) { + return null; + } else { + return input.toString(); + } + } + + /** + * Converts the input to a string with special missing value handling. + */ + private String booleanToString(Boolean input) { + if (input == null) { + return null; + } else { + return input.toString(); + } + + } + + /** + * Converts the input to a string with special missing value handling and integers represented + * as integers if possible. + */ + private String doubleToString(double input, boolean isInteger) { + if (Double.isNaN(input)) { + return null; + } + if (isInteger && input == (int) input) { + return "" + (int) input; + } else { + return "" + input; + } + } + + /** + * Converts the ExpressionType to the name of the constant that the user should use to mark this + * type. + * + * @param type + * an {@link ExpressionType} + * @return the string name of the constant associated to this type + */ + private String getConstantName(ExpressionType type) { + return TypeConstants.INSTANCE.getNameForType(type); + } + + /** + * Converts the type constant passed by the user to an {@link ExpressionType}. + * + * @param expressionEvaluator + * the evaluator holding the type constant. + * @return + */ + private ExpressionType getExpectedReturnType(ExpressionEvaluator expressionEvaluator) { + if (!expressionEvaluator.isConstant()) { + String validTypeArguments = TypeConstants.INSTANCE.getValidConstantsString(); + throw new FunctionInputException("expression_parser.eval.type_not_constant", getFunctionName(), + validTypeArguments); + } + String typeString = null; + try { + typeString = expressionEvaluator.getStringFunction().call(); + } catch (ExpressionParsingException e) { + throw e; + } catch (Exception e) { + throw new ExpressionParsingException(e); + } + + ExpressionType expectedType = TypeConstants.INSTANCE.getTypeForName(typeString); + if (expectedType == null) { + String validTypeArguments = TypeConstants.INSTANCE.getValidConstantsString(); + throw new FunctionInputException("expression_parser.eval.invalid_type", typeString, getFunctionName(), + validTypeArguments); + } + return expectedType; + } + + /** + * Creates an {@link ExpressionEvaluator} with a date callable that calls + * {@link #computeAndCheckType(ExpressionEvaluator, ExpressionType)}. + */ + private ExpressionEvaluator makeDateEvaluator(final ExpressionType expectedType, + final ExpressionEvaluator inputEvaluator) { + Callable dateCallable = () -> { + ExpressionEvaluator subExpressionEvaluator = computeAndCheckType(inputEvaluator, expectedType); + return subExpressionEvaluator.getDateFunction().call(); + }; + return new SimpleExpressionEvaluator(expectedType, dateCallable, false); + } + + /** + * Creates an {@link ExpressionEvaluator} with a boolean callable that calls + * {@link #computeAndCheckType(ExpressionEvaluator, ExpressionType)}. + */ + private ExpressionEvaluator makeBooleanEvaluator(final ExpressionType expectedType, + final ExpressionEvaluator inputEvaluator) { + Callable booleanCallable = () -> { + ExpressionEvaluator subExpressionEvaluator = computeAndCheckType(inputEvaluator, expectedType); + return subExpressionEvaluator.getBooleanFunction().call(); + }; + return new SimpleExpressionEvaluator(booleanCallable, false, expectedType); + } + + /** + * Creates an {@link ExpressionEvaluator} with a double callable that calls + * {@link #computeAndCheckType(ExpressionEvaluator, ExpressionType)}. + */ + private ExpressionEvaluator makeDoubleEvaluator(final ExpressionType expectedType, + final ExpressionEvaluator inputEvaluator) { + DoubleCallable doubleCallable = () -> { + ExpressionEvaluator subExpressionEvaluator = computeAndCheckType(inputEvaluator, expectedType); + return subExpressionEvaluator.getDoubleFunction().call(); + }; + return new SimpleExpressionEvaluator(doubleCallable, expectedType, false); + } + + /** + * Creates an {@link ExpressionEvaluator} with a string callable that calls + * {@link #computeAndCheckType(ExpressionEvaluator, ExpressionType)}. + */ + private ExpressionEvaluator makeStringEvaluator(final ExpressionType expectedType, + final ExpressionEvaluator inputEvaluator) { + Callable stringCallable = () -> { + ExpressionEvaluator subExpressionEvaluator = computeAndCheckType(inputEvaluator, expectedType); + return subExpressionEvaluator.getStringFunction().call(); + }; + return new SimpleExpressionEvaluator(stringCallable, expectedType, false); + } + +} diff --git a/src/main/java/com/rapidminer/tools/expression/internal/function/eval/AttributeEvaluation.java b/src/main/java/com/rapidminer/tools/expression/internal/function/eval/AttributeEvaluation.java new file mode 100644 index 000000000..5d7d0c284 --- /dev/null +++ b/src/main/java/com/rapidminer/tools/expression/internal/function/eval/AttributeEvaluation.java @@ -0,0 +1,80 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General + * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses/. + */ +package com.rapidminer.tools.expression.internal.function.eval; + +import com.rapidminer.tools.expression.Expression; +import com.rapidminer.tools.expression.ExpressionContext; +import com.rapidminer.tools.expression.ExpressionEvaluator; +import com.rapidminer.tools.expression.Function; + + +/** + * A {@link Function} that evaluates subexpressions as attributes. + *

      + * If the first input of the attribute eval function is constant, the attribute is fixed and a fixed {@link + * ExpressionEvaluator} for that attribute is constructed. This means that the evaluation of the {@link Expression}s + * generated from {@code attribute("att"+(4+3))}, {@code attribute("att" + 7)} and {@code attribute("att7")} have the + * same complexity at evaluation time. + *

      + * If the first argument is not constant, then a second argument is needed to determine the result type and the parser + * is called every time the resulting {@link Expression} is evaluated. In particular, evaluating {@link Expression}s + * such as {@code attribute("att"+[att1], REAL)} is slower than the examples above. + * + * @author Gisa Meier + * @since 9.3.0 + */ +public class AttributeEvaluation extends AbstractEvaluation { + + private ExpressionContext context; + + /** + * Creates a evaluation {@link Function}. Before this functions {@link #compute(ExpressionEvaluator...)} method can + * be called, a context needs to be set via {@link #setContext(ExpressionContext)}. + */ + public AttributeEvaluation() { + super("process.attribute"); + } + + /** + * Sets the context that this evaluation function should use. This must always be done before using {@link + * #compute(ExpressionEvaluator...)}. + * + * @param context + * the context to use + */ + public void setContext(ExpressionContext context) { + this.context = context; + } + + @Override + protected void checkSetup() { + if (context == null) { + throw new IllegalStateException("context must be set in order to evaluate"); + } + } + + @Override + protected ExpressionEvaluator compute(String expressionString) { + ExpressionEvaluator evaluator = context.getDynamicVariable(expressionString); + if (evaluator == null) { + throw new AttributeEvaluationException(getFunctionName(), expressionString); + } + return evaluator; + } +} diff --git a/src/main/java/com/rapidminer/tools/expression/internal/function/eval/AttributeEvaluationException.java b/src/main/java/com/rapidminer/tools/expression/internal/function/eval/AttributeEvaluationException.java new file mode 100644 index 000000000..47f7299b4 --- /dev/null +++ b/src/main/java/com/rapidminer/tools/expression/internal/function/eval/AttributeEvaluationException.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2001-2019 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. +*/ +package com.rapidminer.tools.expression.internal.function.eval; + +import com.rapidminer.tools.I18N; +import com.rapidminer.tools.expression.ExpressionParsingException; + + +/** + * A {@link ExpressionParsingException} that is thrown when the function {@link AttributeEvaluation} fails to + * find the attribute specified by the inner expression. + * + * @author Gisa Meier + * @since 9.3.0 + */ +public class AttributeEvaluationException extends ExpressionParsingException { + + private static final long serialVersionUID = -7644715146786931281L; + + /** + * Creates a {@link AttributeEvaluationException} with a message generated from functionName and subExpression. + * + * @param functionName + * the name of the {@link AttributeEvaluation} function + * @param subExpression + * the subexpression for which the {@link AttributeEvaluation} function failed + */ + AttributeEvaluationException(String functionName, String subExpression) { + super(I18N.getErrorMessage("expression_parser.attribute_eval_failed", functionName, subExpression)); + } + +} diff --git a/src/main/java/com/rapidminer/tools/expression/internal/function/eval/Evaluation.java b/src/main/java/com/rapidminer/tools/expression/internal/function/eval/Evaluation.java index 95b75c3c1..64f2088e1 100644 --- a/src/main/java/com/rapidminer/tools/expression/internal/function/eval/Evaluation.java +++ b/src/main/java/com/rapidminer/tools/expression/internal/function/eval/Evaluation.java @@ -18,28 +18,21 @@ */ package com.rapidminer.tools.expression.internal.function.eval; -import java.util.Date; -import java.util.concurrent.Callable; - -import com.rapidminer.tools.Ontology; -import com.rapidminer.tools.expression.DoubleCallable; +import com.rapidminer.tools.expression.Expression; import com.rapidminer.tools.expression.ExpressionEvaluator; import com.rapidminer.tools.expression.ExpressionException; import com.rapidminer.tools.expression.ExpressionParsingException; import com.rapidminer.tools.expression.ExpressionType; -import com.rapidminer.tools.expression.FunctionDescription; -import com.rapidminer.tools.expression.FunctionInputException; +import com.rapidminer.tools.expression.Function; import com.rapidminer.tools.expression.internal.SimpleExpressionEvaluator; import com.rapidminer.tools.expression.internal.antlr.AntlrParser; -import com.rapidminer.tools.expression.internal.function.AbstractFunction; - /** * A {@link Function} that evaluates subexpressions using an {@link AntlrParser}. *

      * If the first input of the eval function is constant, the parser is only called once during the * callable-creation step and not again when the resulting {@link Expression} is evaluated. This - * means that the evaluation of the {@link Expressions} generated from {@code eval("4+3")*[att1]}, + * means that the evaluation of the {@link Expression}s generated from {@code eval("4+3")*[att1]}, * {@code eval("(4+3)*[att1]")} and {@code 7*[att1]} have the same complexity at evaluation time. *

      * If the first argument is not constant, then a second argument is needed to determine the result @@ -50,7 +43,7 @@ * @author Gisa Schaefer * */ -public class Evaluation extends AbstractFunction { +public class Evaluation extends AbstractEvaluation { private AntlrParser parser; @@ -60,7 +53,7 @@ public class Evaluation extends AbstractFunction { * {@link #setParser(AntlrParser)}. */ public Evaluation() { - super("process.eval", FunctionDescription.UNFIXED_NUMBER_OF_ARGUMENTS, Ontology.ATTRIBUTE_VALUE); + super("process.eval"); } /** @@ -75,85 +68,14 @@ public void setParser(AntlrParser parser) { } @Override - public ExpressionEvaluator compute(final ExpressionEvaluator... inputEvaluators) { + protected void checkSetup() { if (parser == null) { throw new IllegalStateException("parser must be set in order to evaluate"); } - // check for string inputs - getResultType(inputEvaluators); - - // Note that if inputEvaluators[0] is constant then this does not mean the result of eval is - // constant, it only means that the return type is constant. Example: 'eval("attribute_1")' - // has the constant string input "attribute_1", but will result in a non-constant evaluator - // that returns the different values of the attribute. - - if (inputEvaluators.length == 1) { - - if (!inputEvaluators[0].isConstant()) { - throw new FunctionInputException("expression_parser.eval.non_constant_single_argument", getFunctionName()); - } - // if eval has one constant argument compute it once using the parser - return compute(inputEvaluators[0]); - - } else if (inputEvaluators.length == 2) { - - if (inputEvaluators[0].isConstant()) { - // if eval has one constant argument and one type argument compute result of first - // argument once using the parser and check the type - return computeAndCheckType(inputEvaluators[0], getExpectedReturnType(inputEvaluators[1])); - } else { - // if eval has one non-constant argument and one type argument create callables - // depending on the type that do the same in the case above - final ExpressionType expectedType = getExpectedReturnType(inputEvaluators[1]); - switch (expectedType) { - case DATE: - return makeDateEvaluator(expectedType, inputEvaluators[0]); - case DOUBLE: - case INTEGER: - return makeDoubleEvaluator(expectedType, inputEvaluators[0]); - case BOOLEAN: - return makeBooleanEvaluator(expectedType, inputEvaluators[0]); - case STRING: - default: - return makeStringEvaluator(expectedType, inputEvaluators[0]); - } - } - - } else { - throw new FunctionInputException("expression_parser.function_wrong_input_two", getFunctionName(), 1, 2, - inputEvaluators.length); - } - } @Override - protected ExpressionType computeType(ExpressionType... inputTypes) { - // check if inputs are strings, otherwise throw exception - for (ExpressionType type : inputTypes) { - if (!(type == ExpressionType.STRING)) { - throw new FunctionInputException("expression_parser.function_wrong_type", getFunctionName(), "nominal"); - } - } - // is not used to compute the resultType, only used to check the input types - return null; - } - - /** - * Evaluates the expression into a String and feeds it to the parser. - * - * @param subexpressionEvaluator - * the evaluator whose string function call yields the expression string - * @return the result of parsing the expression string - */ - private ExpressionEvaluator compute(ExpressionEvaluator subexpressionEvaluator) { - String expressionString = null; - try { - expressionString = subexpressionEvaluator.getStringFunction().call(); - } catch (ExpressionParsingException e) { - throw e; - } catch (Exception e) { - throw new ExpressionParsingException(e); - } + protected ExpressionEvaluator compute(String expressionString) { // if subexpression is a missing nominal don't feed it into the parser if (expressionString == null) { return new SimpleExpressionEvaluator((String) null, ExpressionType.STRING); @@ -163,235 +85,8 @@ private ExpressionEvaluator compute(ExpressionEvaluator subexpressionEvaluator) } catch (ExpressionParsingException | ExpressionException e) { throw new SubexpressionEvaluationException(getFunctionName(), expressionString, e); } - } - - /** - * Evaluates the expression into a String, feeds it to the parser and checks if the resulting - * type is the expected type. If the resulting type is not as expected and the expected type is - * String then converts the result to a string evaluator. If the expected type is double and the - * result type is integer the result type is changed to double. - * - * @param subexpressionEvaluator - * the evaluator whose string function call yields the expression string - * @param expectedType - * the expected type of the result - * @return the result of parsing the expression string - */ - private ExpressionEvaluator computeAndCheckType(ExpressionEvaluator subexpressionEvaluator, ExpressionType expectedType) { - ExpressionEvaluator outEvaluator = compute(subexpressionEvaluator); - if (outEvaluator.getType() == expectedType) { - return outEvaluator; - } else if (expectedType == ExpressionType.DOUBLE && outEvaluator.getType() == ExpressionType.INTEGER) { - // use same resulting evaluator but with different type - return new SimpleExpressionEvaluator(outEvaluator.getDoubleFunction(), expectedType, outEvaluator.isConstant()); - } else if (expectedType == ExpressionType.STRING) { - return convertToStringEvaluator(outEvaluator); - } else { - throw new FunctionInputException("expression_parser.eval.type_not_matching", getFunctionName(), - getConstantName(expectedType), getConstantName(outEvaluator.getType())); - } - } - - /** - * Converts the outEvaluator into an {@link ExpressionEvaluator} of type string. If outEvaluator - * is constant the result is also constant. - * - * @param outEvaluator - * a evaluator which is not of type String - * @return an {@link ExpressionEvaluator} of type String - */ - private ExpressionEvaluator convertToStringEvaluator(final ExpressionEvaluator outEvaluator) { - if (outEvaluator.isConstant()) { - try { - String stringValue = getStringValue(outEvaluator); - return new SimpleExpressionEvaluator(stringValue, ExpressionType.STRING); - } catch (ExpressionParsingException e) { - throw e; - } catch (Exception e) { - throw new ExpressionParsingException(e); - } - } else { - Callable stringCallable = new Callable() { - @Override - public String call() throws Exception { - return getStringValue(outEvaluator); - } - - }; - return new SimpleExpressionEvaluator(stringCallable, ExpressionType.STRING, false); - } } - /** - * Calculates the String value that the outEvaluator should return. - */ - private String getStringValue(ExpressionEvaluator outEvaluator) throws Exception { - switch (outEvaluator.getType()) { - case DOUBLE: - case INTEGER: - return doubleToString(outEvaluator.getDoubleFunction().call(), - outEvaluator.getType() == ExpressionType.INTEGER); - case BOOLEAN: - return booleanToString(outEvaluator.getBooleanFunction().call()); - case DATE: - return dateToString(outEvaluator.getDateFunction().call()); - default: - // cannot happen - return null; - } - } - - /** - * Converts the input to a string with special missing value handling - */ - private String dateToString(Date input) { - if (input == null) { - return null; - } else { - return input.toString(); - } - } - - /** - * Converts the input to a string with special missing value handling. - */ - private String booleanToString(Boolean input) { - if (input == null) { - return null; - } else { - return input.toString(); - } - - } - - /** - * Converts the input to a string with special missing value handling and integers represented - * as integers if possible. - */ - private String doubleToString(double input, boolean isInteger) { - if (Double.isNaN(input)) { - return null; - } - if (isInteger && input == (int) input) { - return "" + (int) input; - } else { - return "" + input; - } - } - - /** - * Converts the ExpressionType to the name of the constant that the user should use to mark this - * type. - * - * @param type - * an {@link ExpressionType} - * @return the string name of the constant associated to this type - */ - private String getConstantName(ExpressionType type) { - return TypeConstants.INSTANCE.getNameForType(type); - } - - /** - * Converts the type constant passed by the user to an {@link ExpressionType}. - * - * @param expressionEvaluator - * the evaluator holding the type constant. - * @return - */ - private ExpressionType getExpectedReturnType(ExpressionEvaluator expressionEvaluator) { - if (!expressionEvaluator.isConstant()) { - String validTypeArguments = TypeConstants.INSTANCE.getValidConstantsString(); - throw new FunctionInputException("expression_parser.eval.type_not_constant", getFunctionName(), - validTypeArguments); - } - String typeString = null; - try { - typeString = expressionEvaluator.getStringFunction().call(); - } catch (ExpressionParsingException e) { - throw e; - } catch (Exception e) { - throw new ExpressionParsingException(e); - } - - ExpressionType expectedType = TypeConstants.INSTANCE.getTypeForName(typeString); - if (expectedType == null) { - String validTypeArguments = TypeConstants.INSTANCE.getValidConstantsString(); - throw new FunctionInputException("expression_parser.eval.invalid_type", typeString, getFunctionName(), - validTypeArguments); - } - return expectedType; - } - - /** - * Creates an {@link ExpressionEvaluator} with a date callable that calls - * {@link #computeAndCheckType(ExpressionEvaluator, ExpressionType)}. - */ - private ExpressionEvaluator makeDateEvaluator(final ExpressionType expectedType, final ExpressionEvaluator inputEvaluator) { - Callable dateCallable = new Callable() { - - @Override - public Date call() throws Exception { - ExpressionEvaluator subExpressionEvaluator = computeAndCheckType(inputEvaluator, expectedType); - return subExpressionEvaluator.getDateFunction().call(); - } - - }; - return new SimpleExpressionEvaluator(expectedType, dateCallable, false); - } - - /** - * Creates an {@link ExpressionEvaluator} with a boolean callable that calls - * {@link #computeAndCheckType(ExpressionEvaluator, ExpressionType)}. - */ - private ExpressionEvaluator makeBooleanEvaluator(final ExpressionType expectedType, - final ExpressionEvaluator inputEvaluator) { - Callable booleanCallable = new Callable() { - - @Override - public Boolean call() throws Exception { - ExpressionEvaluator subExpressionEvaluator = computeAndCheckType(inputEvaluator, expectedType); - return subExpressionEvaluator.getBooleanFunction().call(); - } - - }; - return new SimpleExpressionEvaluator(booleanCallable, false, expectedType); - } - - /** - * Creates an {@link ExpressionEvaluator} with a double callable that calls - * {@link #computeAndCheckType(ExpressionEvaluator, ExpressionType)}. - */ - private ExpressionEvaluator makeDoubleEvaluator(final ExpressionType expectedType, - final ExpressionEvaluator inputEvaluator) { - DoubleCallable doubleCallable = new DoubleCallable() { - - @Override - public double call() throws Exception { - ExpressionEvaluator subExpressionEvaluator = computeAndCheckType(inputEvaluator, expectedType); - return subExpressionEvaluator.getDoubleFunction().call(); - } - - }; - return new SimpleExpressionEvaluator(doubleCallable, expectedType, false); - } - - /** - * Creates an {@link ExpressionEvaluator} with a string callable that calls - * {@link #computeAndCheckType(ExpressionEvaluator, ExpressionType)}. - */ - private ExpressionEvaluator makeStringEvaluator(final ExpressionType expectedType, - final ExpressionEvaluator inputEvaluator) { - Callable stringCallable = new Callable() { - - @Override - public String call() throws Exception { - ExpressionEvaluator subExpressionEvaluator = computeAndCheckType(inputEvaluator, expectedType); - return subExpressionEvaluator.getStringFunction().call(); - } - - }; - return new SimpleExpressionEvaluator(stringCallable, expectedType, false); - } } diff --git a/src/main/java/com/rapidminer/tools/expression/internal/function/eval/TypeConstants.java b/src/main/java/com/rapidminer/tools/expression/internal/function/eval/TypeConstants.java index 75cac6417..59ac26da2 100644 --- a/src/main/java/com/rapidminer/tools/expression/internal/function/eval/TypeConstants.java +++ b/src/main/java/com/rapidminer/tools/expression/internal/function/eval/TypeConstants.java @@ -30,7 +30,7 @@ /** - * Type constants that can be used as a second argument in the eval function. + * Type constants that can be used as a second argument in the eval and attribute eval function. * * @author Gisa Schaefer * @@ -39,7 +39,7 @@ public enum TypeConstants { INSTANCE; - private static final String USED_IN_EVAL = "used in eval"; + private static final String USED_IN_EVAL = "used in eval and attribute eval"; private final List typeConstants = new ArrayList<>(5); private final Map conversionMap = new HashMap<>(5); diff --git a/src/main/java/com/rapidminer/tools/math/ROCDataGenerator.java b/src/main/java/com/rapidminer/tools/math/ROCDataGenerator.java index 3fe54a38e..e6209358e 100644 --- a/src/main/java/com/rapidminer/tools/math/ROCDataGenerator.java +++ b/src/main/java/com/rapidminer/tools/math/ROCDataGenerator.java @@ -77,11 +77,28 @@ public double getBestThreshold() { } /** - * Creates a list of ROC data points from the given example set. The example set must have a - * binary label attribute and confidence values for both values, i.e. a model must have been - * applied on the data. + * Equivalent to calling {@link #createROCData(ExampleSet, boolean, ROCBias, String)} with {@code positiveClassName = null}. */ public ROCData createROCData(ExampleSet exampleSet, boolean useExampleWeights, ROCBias method) { + return createROCData(exampleSet, useExampleWeights, method, null); + } + + /** + * Creates a list of ROC data points from the given example set. The example set must have a binary label attribute + * and confidence values for both values, i.e. a model must have been applied on the data. + * + * @param exampleSet + * An example set with a binary label and corresponding confidence values. + * @param useExampleWeights + * If {@code true}, the weight attribute from the specified example set will used for the calculations. + * @param method + * See {@link ROCBias}. + * @param positiveClassName + * If non-{@code null}, this will be used as the positive class. Otherwise the method will fall back to using the + * labels mapping to decide which is the positive class. + * @return The generated {@link ROCData}. + */ + public ROCData createROCData(ExampleSet exampleSet, boolean useExampleWeights, ROCBias method, String positiveClassName) { Attribute label = exampleSet.getAttributes().getLabel(); exampleSet.recalculateAttributeStatistics(label); Attribute predictedLabel = exampleSet.getAttributes().getPredictedLabel(); @@ -93,8 +110,12 @@ public ROCData createROCData(ExampleSet exampleSet, boolean useExampleWeights, R weightAttr = exampleSet.getAttributes().getWeight(); } Attribute labelAttr = exampleSet.getAttributes().getLabel(); - String positiveClassName = null; - int positiveIndex = label.getMapping().getPositiveIndex(); + + int positiveIndex = positiveClassName != null ? label.getMapping().getIndex(positiveClassName) : + label.getMapping().getPositiveIndex(); + int negativeIndex = positiveIndex == label.getMapping().getPositiveIndex() ? + label.getMapping().getNegativeIndex() : label.getMapping().getPositiveIndex(); + if (label.isNominal() && (label.getMapping().size() == 2)) { positiveClassName = labelAttr.getMapping().mapIndex(positiveIndex); } else if (label.isNominal() && (label.getMapping().size() == 1)) { @@ -103,6 +124,7 @@ public ROCData createROCData(ExampleSet exampleSet, boolean useExampleWeights, R throw new AttributeTypeException( "Cannot calculate ROC data for non-classification labels or for labels with more than 2 classes."); } + int index = 0; Iterator reader = exampleSet.iterator(); while (reader.hasNext()) { @@ -117,14 +139,37 @@ public ROCData createROCData(ExampleSet exampleSet, boolean useExampleWeights, R } calArray[index++] = wcl; } - Arrays.sort(calArray, new WeightedConfidenceAndLabel.WCALComparator(method)); + Arrays.sort(calArray, new WeightedConfidenceAndLabel.WCALComparator(method) { + /** + * Compares two {@link WeightedConfidenceAndLabel}s based on their confidence using + * {@link Double#compare(double, double)}. If the confidence is equal, the labels are compared + * according to the chosen {@link ROCBias}. + */ + @Override + public int compare(WeightedConfidenceAndLabel o1, WeightedConfidenceAndLabel o2) { + int compi = (-1) * Double.compare(o1.getConfidence(), o2.getConfidence()); + if (compi == 0) { + switch (method) { + case OPTIMISTIC: + return positiveIndex == 1 ? -Double.compare(o1.getLabel(), o2.getLabel()) : Double.compare(o1.getLabel(), o2.getLabel()); + case PESSIMISTIC: + return positiveIndex == 1 ? Double.compare(o1.getLabel(), o2.getLabel()) : -Double.compare(o1.getLabel(), o2.getLabel()); + case NEUTRAL: + default: + return Double.compare(o1.getLabel(), o2.getLabel()); + } + } else { + return compi; + } + } + }); // The slope is defined by the ratio of positive examples and the // different misclassification costs. // The formula for the slope is (#pos / #neg) / (costs_neg / costs_pos). double ratio = exampleSet.getStatistics(label, Statistics.COUNT, positiveClassName) / exampleSet.getStatistics(label, Statistics.COUNT, - label.getMapping().mapIndex(label.getMapping().getNegativeIndex())); + label.getMapping().mapIndex(negativeIndex)); slope = misclassificationCostsNegative / misclassificationCostsPositive; slope = ratio / slope; @@ -141,7 +186,6 @@ public ROCData createROCData(ExampleSet exampleSet, boolean useExampleWeights, R ROCData rocData = new ROCData(); ROCPoint last = new ROCPoint(0.0d, 0.0d, 1.0d); - // rocData.addPoint(last); // add first point in ROC curve // Iterate through the example set sorted by predictions. // In each iteration the example with next highest confidence of being @@ -176,11 +220,6 @@ public ROCData createROCData(ExampleSet exampleSet, boolean useExampleWeights, R bestThreshold = wcl.getConfidence(); } } - /* - * double currentConfidence = wcl.getConfidence(); if (currentConfidence != - * oldConfidence) { rocData.addPoint(last); oldConfidence = currentConfidence; } last = - * new ROCPoint(fp, tp, currentConfidence); - */ totalWeight += weight; last = new ROCPoint(totalWeight - truePositiveWeight, truePositiveWeight, currentConfidence); @@ -194,16 +233,6 @@ public ROCData createROCData(ExampleSet exampleSet, boolean useExampleWeights, R bestIsometricsTpValue = c; } - // rocData.addPoint(new ROCPoint(sum - tp, tp, 0.0d)); // add last point in ROC curve - // add last point in ROC curve - // if (rocData.getNumberOfPoints() == 1) { - // rocData.addPoint(new ROCPoint(totalWeight - truePositiveWeight, truePositiveWeight, - // oldConfidence)); - // } else { - // rocData.addPoint(new ROCPoint(totalWeight - truePositiveWeight, truePositiveWeight, - // 0.0d)); - // } - // scaling for plotting rocData.setTotalPositives(truePositiveWeight); rocData.setTotalNegatives(totalWeight - truePositiveWeight); @@ -309,17 +338,12 @@ public double calculateAUC(ROCData rocData) { tpDivP = point.getTruePositives() / rocData.getTotalPositives(); } - /* - * if (last != null) { aucSum += ((tpDivP - last[1]) * (fpDivN - last[0]) / 2.0d) + - * (last[1] * (fpDivN - last[0])); } - */ if (last != null) { double width = fpDivN - last[0]; double leftHeight = last[1]; double rightHeight = tpDivP; aucSum += leftHeight * width + (rightHeight - leftHeight) * width / 2; - // aucSum += leftHeight * width; } last = new double[]{fpDivN, tpDivP}; } diff --git a/src/main/java/com/rapidminer/tools/parameter/admin/ParameterEnforcer.java b/src/main/java/com/rapidminer/tools/parameter/admin/ParameterEnforcer.java index 702236b54..0785572a4 100644 --- a/src/main/java/com/rapidminer/tools/parameter/admin/ParameterEnforcer.java +++ b/src/main/java/com/rapidminer/tools/parameter/admin/ParameterEnforcer.java @@ -29,9 +29,9 @@ import java.util.function.BiConsumer; import java.util.function.Function; import java.util.logging.Level; -import javax.swing.SwingWorker; import javax.swing.Timer; +import com.rapidminer.gui.tools.MultiSwingWorker; import com.rapidminer.tools.LogService; @@ -245,7 +245,7 @@ private void restartTimer() { * Reloads the properties and starts the next timer */ private void readAndRestart() { - new SwingWorker() { + new MultiSwingWorker() { @Override protected Void doInBackground() { @@ -257,7 +257,7 @@ protected Void doInBackground() { restartTimer(); return null; } - }.execute(); + }.start(); } } diff --git a/src/main/java/com/rapidminer/tools/plugin/Plugin.java b/src/main/java/com/rapidminer/tools/plugin/Plugin.java index 066ce1617..a9fcef396 100644 --- a/src/main/java/com/rapidminer/tools/plugin/Plugin.java +++ b/src/main/java/com/rapidminer/tools/plugin/Plugin.java @@ -1327,6 +1327,39 @@ public static void initPluginTests() { callPluginInitMethods("initPluginTests", new Class[] {}, new Object[] {}, false); } + /** + * Finds the given object's {@link Plugin} if possible. Returns {@code null} for objects whose classes were + * not loaded through a {@link PluginClassLoader}. + * + * @param o + * the object to check + * @return the plugin associated with the given object or {@code null} if it was not loaded through a plugin + * @since 9.3 + */ + public static Plugin getPluginForObject(Object o) { + if (o == null) { + return null; + } + return getPluginForClass(o.getClass()); + } + + /** + * Finds the given class' {@link Plugin} if possible. Returns {@code null} for classes that were + * not loaded through a {@link PluginClassLoader}. + * + * @param c + * the class to check + * @return the plugin associated with the given object or {@code null} if it was not loaded through a plugin + * @since 9.3 + */ + public static Plugin getPluginForClass(Class c) { + ClassLoader cl = c.getClassLoader(); + if (cl instanceof PluginClassLoader) { + return getPluginByExtensionId(((PluginClassLoader) cl).getPluginKey()); + } + return null; + } + private static void callPluginInitMethods(String methodName, Class[] arguments, Object[] argumentValues, boolean useOriginalJarClassLoader) { for (Plugin plugin : PLUGIN_INITIALIZATION_ORDER) { diff --git a/src/main/java/com/rapidminer/tools/usagestats/ActionStatisticsCollector.java b/src/main/java/com/rapidminer/tools/usagestats/ActionStatisticsCollector.java index b8caef4d1..c120cef51 100644 --- a/src/main/java/com/rapidminer/tools/usagestats/ActionStatisticsCollector.java +++ b/src/main/java/com/rapidminer/tools/usagestats/ActionStatisticsCollector.java @@ -40,6 +40,7 @@ import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; + import javax.swing.AbstractButton; import javax.swing.Action; import javax.swing.JToggleButton; @@ -51,6 +52,9 @@ import com.rapidminer.Process; import com.rapidminer.ProcessListener; import com.rapidminer.RapidMiner; +import com.rapidminer.connection.ConnectionInformation; +import com.rapidminer.connection.configuration.ConnectionConfiguration; +import com.rapidminer.connection.valueprovider.ValueProvider; import com.rapidminer.example.ExampleSet; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.tools.ResourceAction; @@ -103,6 +107,7 @@ public static interface UsageObject { public static final String TYPE_ERROR = "error"; public static final String TYPE_IMPORT = "import"; public static final String TYPE_DIALOG = "dialog"; + public static final String TYPE_INJECT_VALUE_PROVIDER_DIALOG = "inject_vp_dialog"; public static final String TYPE_CONSTRAINT = "constraint"; public static final String TYPE_LICENSE_LEVEL = "license-level"; public static final String TYPE_PROGRESS_THREAD = "progress-thread"; @@ -258,6 +263,8 @@ public static interface UsageObject { /** remote_repository | status | uuid (since 8.2.1) */ public static final String TYPE_REMOTE_REPOSITORY = "remote_repository"; + /** remote_repository_saml | status | uuid (since 9.3) */ + public static final String TYPE_REMOTE_REPOSITORY_SAML = "remote_repository_saml"; /** new_import | guessed_date_wrong | guessed|choosen (since 9.1) */ public static final String VALUE_GUESSED_DATE_FORMAT_RIGHT = "guessed_date_format_right"; @@ -280,6 +287,11 @@ public static interface UsageObject { public static final String TYPE_HTML5_VISUALIZATION_CONFIG_UI = "html5_visualization_config_ui"; public static final String VALUE_CONFIG_GROUP_EXPANDED = "config_group_expanded"; + /** connections (since 9.3) */ + public static final String TYPE_CONNECTION = "connection"; + public static final String TYPE_CONNECTION_TEST = "connection_test"; + public static final String TYPE_OLD_CONNECTION = "old_connection"; + public static final String TYPE_CONNECTION_INJECTION = "connection_injection"; public static final String VALUE_CREATED = "created"; public static final String VALUE_CONNECTED = "connected"; @@ -439,7 +451,7 @@ public void logHTML5VisualizationException(List plotTypes, Throwable t) StringBuilder exception = new StringBuilder(); exception.append("ex").append(ARG_SPACER); exception.append(t.getClass()).append(ARG_SPACER); - exception.append(getThrowablenStackTraceAsString(t)); + exception.append(getThrowableStackTraceAsString(t)); StringBuilder arg = new StringBuilder(); arg.append(String.join(",", plotTypes)).append(ARG_SPACER); @@ -458,7 +470,8 @@ public void logHTML5VisualizationBrowserException(Throwable t) { StringBuilder exception = new StringBuilder(); exception.append("ex").append(ARG_SPACER); exception.append(t.getClass()).append(ARG_SPACER); - exception.append(getThrowablenStackTraceAsString(t)); + exception.append(t.getMessage()).append(ARG_SPACER); + exception.append(getThrowableStackTraceAsString(t)); StringBuilder arg = new StringBuilder(); arg.append(exception.toString()); @@ -495,6 +508,37 @@ public void logHTML5VisualizationBrowserSetupTestFinished(String type, boolean s log(TYPE_HTML5_VISUALIZATION_BROWSER, VALUE_BROWSER_SETUP_TEST_FINISHED, arg.toString()); } + /** + * Logs the usage of a connection in an operator. + * + * @param operator + * the operator where the connection is used + * @param connection + * the connection used in the operator + * @since 9.3.0 + */ + public void logNewConnection(Operator operator, ConnectionInformation connection) { + log(ActionStatisticsCollector.TYPE_OPERATOR, operator.getOperatorDescription().getKey(), + TYPE_CONNECTION + ARG_SPACER + connection.getConfiguration().getType() + ARG_SPACER + + getConnectionInjections(connection.getConfiguration())); + } + + /** + * Logs the usage of an old connection (configurable or database connection configuration) in an operator. + * + * @param operator + * the operator where the old connection is used + * @param oldConnectionType + * the type of the old connection + * @since 9.3.0 + */ + public void logOldConnection(Operator operator, String oldConnectionType) { + log(ActionStatisticsCollector.TYPE_OPERATOR, operator.getOperatorDescription().getKey(), + TYPE_OLD_CONNECTION + ARG_SPACER + oldConnectionType); + } + + + /** * A Key defines an identifier that is used to store some collected usage data associated with it. It has 3 levels, * TYPE, VALUE and ARG, where ARG may be null. @@ -884,7 +928,7 @@ public int hashCode() { * @return */ public static String getExceptionStackTraceAsString(Exception e) { - return getThrowablenStackTraceAsString(e); + return getThrowableStackTraceAsString(e); } /** @@ -895,12 +939,32 @@ public static String getExceptionStackTraceAsString(Exception e) { * @return the stacktrace, never {@code null} * @since 9.2.0 */ - public static String getThrowablenStackTraceAsString(Throwable t) { + public static String getThrowableStackTraceAsString(Throwable t) { if (t == null) { throw new IllegalArgumentException("t must not be null!"); } - return Stream.of(t.getStackTrace()).limit(40).map(StackTraceElement::toString).collect(Collectors.joining(",")); + return Stream.of(t.getStackTrace()).limit(40).map(StackTraceElement::toString).collect(Collectors.joining("," + )); + } + + /** + * Creates a string from the injections for the given connection configuration as the comma-separated injection + * sources followed by {@code |} followed by the comma-separated injection parameter names. + * + * @param configuration + * the configuration of a connection + * @return the injections of the form {@code source1,source2|param1,param2,param3} + */ + public static String getConnectionInjections(ConnectionConfiguration configuration) { + String injectionSources = configuration.getValueProviders().stream() + .map(ValueProvider::getType) + .collect(Collectors.joining("," )); + String injectedParameters = configuration.getKeyMap().entrySet().stream() + .filter(e -> e.getValue().isInjected()) + .map(Entry::getKey) + .collect(Collectors.joining(",")); + return injectionSources + ARG_SPACER + injectedParameters; } /** Listener that logs input and output volume at operator ports. */ diff --git a/src/main/java/com/rapidminer/tools/usagestats/RuleService.java b/src/main/java/com/rapidminer/tools/usagestats/RuleService.java index 5f99edbe0..cb34b5dfa 100644 --- a/src/main/java/com/rapidminer/tools/usagestats/RuleService.java +++ b/src/main/java/com/rapidminer/tools/usagestats/RuleService.java @@ -29,8 +29,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; import com.rapidminer.RapidMiner; import com.rapidminer.RapidMiner.ExecutionMode; import com.rapidminer.RapidMinerVersion; @@ -54,6 +54,8 @@ enum RuleService { private final Pattern prohibitedKeywords; + private final ObjectReader reader; + private final Set rules = new HashSet<>(); private RuleService() { @@ -88,6 +90,8 @@ private RuleService() { // Create a "\b(JOIN|CREATE|...)\b" regex, where \b matches word boundaries this.prohibitedKeywords = Pattern.compile("\\b(" + String.join("|", prohibitedKeywords) + ")\\b", Pattern.CASE_INSENSITIVE); + ObjectMapper mapper = new ObjectMapper(); + this.reader = mapper.reader(mapper.getTypeFactory().constructCollectionType(List.class, Rule.class)); reloadRules(); } @@ -103,7 +107,6 @@ public void reloadRules() { return; } - ObjectMapper mapper = new ObjectMapper(); List loadedRules = null; Iterator ruleProvider = RuleProviderRegistry.INSTANCE.getRuleProvider().iterator(); @@ -111,8 +114,7 @@ public void reloadRules() { RuleProvider provider = ruleProvider.next(); try (InputStream ruleJson = provider.getRuleJson()) { if (ruleJson != null) { - loadedRules = checkAndConvertRules(mapper.readValue(ruleJson, new TypeReference>() { - })); + loadedRules = checkAndConvertRules(reader.readValue(ruleJson)); } else { LogService.getRoot().log(Level.FINE, I18N.getMessage(LogService.getRoot().getResourceBundle(), "com.rapidminer.tools.usagestats.RuleService.load.empty", provider.getClass().getSimpleName())); diff --git a/src/main/java/com/rapidminer/tutorial/Tutorial.java b/src/main/java/com/rapidminer/tutorial/Tutorial.java index 4a3b000cd..195a3acce 100644 --- a/src/main/java/com/rapidminer/tutorial/Tutorial.java +++ b/src/main/java/com/rapidminer/tutorial/Tutorial.java @@ -36,7 +36,9 @@ import com.rapidminer.io.process.ProcessOriginProcessXMLFilter; import com.rapidminer.io.process.ProcessOriginProcessXMLFilter.ProcessOriginState; import com.rapidminer.operator.FlagUserData; +import com.rapidminer.repository.IOObjectEntry; import com.rapidminer.repository.MalformedRepositoryLocationException; +import com.rapidminer.repository.ProcessEntry; import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; import com.rapidminer.repository.resource.ZipStreamResource; @@ -273,9 +275,9 @@ private void load() throws IOException, RepositoryException { defaultProps.load(zip); } else if (localeFileName.equals(entryName.replaceFirst(folder, ""))) { localProps.load(zip); - } else if (entryName.endsWith(".rmp")) { + } else if (entryName.endsWith(ProcessEntry.RMP_SUFFIX)) { processName = Paths.get(entryName).getFileName().toString().split("\\.")[0]; - } else if (entryName.endsWith(".ioo")) { + } else if (entryName.endsWith(IOObjectEntry.IOO_SUFFIX)) { demoData.add(Paths.get(entryName).getFileName().toString().split("\\.")[0]); } } diff --git a/src/main/resources/com/rapidminer/resources/EULA_EN.txt b/src/main/resources/com/rapidminer/resources/EULA_EN.txt index ed00dd41c..088c3ab30 100644 --- a/src/main/resources/com/rapidminer/resources/EULA_EN.txt +++ b/src/main/resources/com/rapidminer/resources/EULA_EN.txt @@ -1,83 +1,91 @@ *** IMPORTANT *** PLEASE READ CAREFULLY BEFORE YOU DOWNLOAD OR USE THE SOFTWARE -This document (the "Agreement") is a legal agreement between RapidMiner, Inc. ("RapidMiner") and you (the "Licensee"). The software that you are downloading and/or using (the "Software") is the exclusive property of RapidMiner or its licensors and is protected by United States and International Intellectual Property Laws. The Software is copyrighted and licensed (not sold). RapidMiner is only willing to license the Software subject to the terms and conditions of this Agreement, and any use of the Software outside of the scope of such terms and conditions is prohibited. +This document (the “Agreement”) is a legal agreement between RapidMiner, Inc. (“RapidMiner”) and you (the “Licensee”). The software that you are downloading and/or using (the “Software”) is the exclusive property of RapidMiner or its licensors and is protected by United States and International Intellectual Property Laws. The Software is copyrighted and licensed (not sold). RapidMiner is only willing to license the Software subject to the terms and conditions of this Agreement, and any use of the Software outside of the scope of such terms and conditions is prohibited. -By clicking on the "accept" button or the respective checkbox at the end of this document or by downloading, installing, copying, executing or otherwise using the Software, you acknowledge that you have read this Agreement, understand it and agree to be solely bound by its terms and conditions. If you are not willing to be solely bound by the terms of this Agreement, do not download or use the Software. +By clicking the button or checkbox at the end of this document or by downloading, installing, copying, executing or otherwise using the Software, you acknowledge that you have read this Agreement, understand it and agree to be bound by its terms and conditions. If you are not willing to be bound by the terms of this Agreement, click the button or checkbox at the end of this document and do not download or use the Software. -If you are using the Software in your capacity as employee or agent of a company or organization, then any references to the "Licensee" in this Agreement shall refer to such entity and not to you in your personal capacity. You warrant that you are authorized to legally bind the Licensee. If you are not so authorized, then neither you nor the Licensee may use the Software in any manner whatsoever. +If you are using the Software in your capacity as employee or agent of a company or organization, then any references to the “Licensee” in this Agreement shall refer to such entity and not to you in your personal capacity. You warrant that you are authorized to legally bind the Licensee. If you are not so authorized, then neither you nor the Licensee may use the Software in any manner whatsoever. -1. Definitions. +1. Definitions. The following capitalized terms used in this Agreement shall have the meanings set forth below: -1.1 "Confidential Information" shall mean all written or oral information, disclosed by RapidMiner to Licensee, related to the operations of either RapidMiner or any third party, that has been identified as confidential or that by the nature of the information or the circumstances surrounding disclosure ought reasonably to be understood to be proprietary and/or confidential. Without limiting the generality of the foregoing, RapidMiner hereby designates the Software, the Deliverables, and any algorithms, mathematical models, business plans, product plans, financial data or other ideas, techniques or information disclosed in the course of providing the Software or the Services as Confidential Information. -1.2 "Deliverable" shall mean any invention, work of authorship, information or other work product, other than the Software, that is provided to Licensee by RapidMiner in the course of performing Services. -1.3 "Documentation" means RapidMiner's then-current manuals, guides, and online help pages, if any, applicable to the Software and made generally available by RapidMiner to its customers at http://docs.rapidminer.com/. -1.4 "Effective Date" shall mean the date upon which the Licensee clicks through or otherwise accepts this Agreement. -1.5 "Extension" means a dynamically-loaded software component that serves to extend the capabilities of the Software. Extensions may be used to create new operators, templates, tutorials, UI components, connection types, or otherwise extend or enhance the existing functionality of the Software. -1.6 "Field of Use" shall mean any geographic, subject matter, or other field-of-use limitation established by the Usage Policy or the applicable Order. -1.7 "License Key" means any unique form of enabling code issued by RapidMiner for activation of the Software. -1.8 "License Term" means the duration of the license for the Software, as set forth in the applicable Order. -1.9 "Order" means the final configuration details for the Software licensed by Licensee. The Order can be represented by a physical order form, executed by both parties, that references this Agreement, or the Order can be represented by the online procurement and/or registration process completed by Licensee. In general, the Order will capture the specific configuration of the Software, the Field of Use, and any other relevant information relating to the Software. -1.10 "RM Extension" means an Extension that is developed and made generally available by RapidMiner. -1.11 "Services" may comprise consulting, training, Support and any other professional services provided to Licensee by RapidMiner. -1.12 "Software" shall mean the software set forth on the applicable Order. Software shall also include the Documentation, any applicable RM Extensions, and any updates or upgrades to the Software provided to Licensee by RapidMiner. -1.13 "Support" means any technical support that RapidMiner makes generally available to users of the Software, as outlined in RapidMiner's then-current Support Policy, available at http://rapidminer.com/support-policy. -1.14 "Third Party Extension" means an Extension that is developed by either the Licensee or a third party. -1.15 "Usage Policy" shall mean RapidMiner's then-current Product Configuration & Usage Policy, available at http://rapidminer.com/product-configuration-usage-policy. +1.1 “Confidential Information” shall mean all written or oral information, disclosed by RapidMiner to Licensee, related to the operations of either RapidMiner or any third party, that has been identified as confidential or that by the nature of the information or the circumstances surrounding disclosure ought reasonably to be understood to be proprietary and/or confidential. Without limiting the generality of the foregoing, RapidMiner hereby designates the Software, the Deliverables, and any algorithms, mathematical models, business plans, product plans, financial data or other ideas, techniques or information disclosed in the course of providing the Software or the Services as Confidential Information. +1.2 “Deliverable” shall mean any invention, work of authorship, information or other work product, other than the Software, that is provided to Licensee by RapidMiner in the course of performing Services. +1.3 “Documentation” means RapidMiner’s then-current manuals, guides, and online help pages, if any, applicable to the Software and made generally available by RapidMiner to its customers at http://docs.rapidminer.com/. +1.4 “Effective Date” shall mean the date upon which the Licensee clicks through or otherwise accepts this Agreement. +1.5 “Extension” means a dynamically-loaded software component that serves to extend the capabilities of the Software. Extensions may be used to create new operators, templates, tutorials, UI components, connection types, or otherwise extend or enhance the existing functionality of the Software. +1.6 “Field of Use” shall mean any geographic, subject matter, or other field-of-use limitation established by the Usage Policy or the applicable Order. +1.7 “License Key” means any unique form of enabling code issued by RapidMiner for activation of the Software. +1.8 “License Term” means the duration of the license for the Software, as set forth in the applicable Order. +1.9 “Order” means the final configuration details for the Software licensed by Licensee. The Order can be represented by a physical order form, executed by both parties, that references this Agreement, or the Order can be represented by the online procurement and/or registration process completed by Licensee. In general, the Order will capture the specific configuration of the Software, the Field of Use, and any other relevant information relating to the Software. +1.10 “RM Extension” means an Extension that is developed and made generally available by RapidMiner. +1.11 “Services” may comprise consulting, training, Support and any other professional services provided to Licensee by RapidMiner. +1.12 “Software” shall mean the software set forth on the applicable Order. Software shall also include the Documentation, any applicable RM Extensions, and any updates or upgrades to the Software provided to Licensee by RapidMiner. +1.13 “Support” means any technical support that RapidMiner makes generally available to users of the Software, as outlined in RapidMiner’s then-current Support Policy, available at http://rapidminer.com/support-policy. +1.14 “Third Party Extension” means an Extension that is developed by either the Licensee or a third party. +1.15 “Usage Policy” shall mean RapidMiner’s then-current Product Configuration & Usage Policy, available at http://rapidminer.com/product-configuration-usage-policy. 2. Limited License. -2.1 License Grant. Subject to the terms and conditions of this Agreement, RapidMiner hereby grants to Licensee, during the applicable License Term, a non-exclusive, non-transferable, non-sublicensable right and license to (a) execute the Software within the Field of Use, solely for Licensee's internal use and (b) make a reasonable number of copies of the Software solely for non-productive, archival purposes. +2.1 License Grant. Subject to the terms and conditions of this Agreement, RapidMiner hereby grants to Licensee, during the applicable License Term, a non-exclusive, non-transferable, non-sublicensable right and license to (a) execute the Software within the Field of Use, solely for Licensee’s internal use and (b) make a reasonable number of copies of the Software solely for non-productive, archival purposes. 2.2 Limited License for Extensions. Notwithstanding the prohibition on derivative works set forth in Section 2.3(iii) below, RapidMiner hereby grants to Licensee, during the applicable License Term and within the applicable Field of Use, a non-exclusive, non-transferable, non-sublicensable right and license to combine Extensions with the Software, provided that in no event may an Extension be used to convert the client version of the Software into a server-based application, remove or hide existing UI components of the Software, modify any RapidMiner branding of the Software, or otherwise re-purpose the Software for a usage pattern other than it was intended for. -2.3 Restrictions. The Software shall not be used for any purpose other than as expressly authorized by this Agreement. In particular, but without limitation, Licensee shall not, nor permit any third party to: (i) assign, sublicense, market, sell, lease, rent, distribute, convey or otherwise transfer or make the Software available to, or use the Software on behalf of, any third party; (ii) adapt, alter, modify, or translate the Software; (iii) create derivative works of the Software; (iv) reverse engineer, decompile, disassemble or otherwise attempt to reconstruct or obtain the source code to all or any portion of the Software for any purpose; (v) pledge as security or otherwise encumber the rights and licenses granted hereunder with respect to the Software; (vi) use the Software in any service bureau, time sharing, rental, or application service provider arrangement, as such terms are ordinarily understood within the software industry, or otherwise provide access to the Software to third parties; (vii) use the Software in any manner not in compliance with all applicable laws, regulations, and rules; (viii) use the Software, or cause the Software to be used, on any device not owned, operated, or controlled by Licensee; (ix) tamper with or modify any License Key, or otherwise attempt to use the Software outside of its prescribed Field of Use; (x) disclose benchmark results, competitive analyses, or any other information regarding the performance, design, functionality or features of the Software; or (xi) develop any product or service that competes with the Software or with RapidMiner. -2.4 Reservation of Rights. Nothing in this Agreement shall be deemed to grant Licensee, either directly or by implication, estoppel, or otherwise, any license or rights other than those expressly granted in Sections 2.1 and 2.2 of this Agreement. By virtue of this Agreement, Licensee acquires only the right to use the Software and does not acquire any other rights or ownership interests. RapidMiner reserves all rights to the Software not expressly granted to Licensee under this Agreement. RapidMiner shall retain all right, title, and interest in and to the Software, the Services, the Deliverables, the Confidential Information, and any improvements to any of the foregoing (including without limitation any improvements suggested by Licensee or by Licensee's usage of the Software), as well as any other invention, modification, discovery, design, development, improvement, process, algorithm, software, documentation, formula, data, technique, know-how or other invention, innovation or work of authorship, or any interest therein (whether or not patentable or registrable under copyright or similar statutes or subject to analogous protection) discovered, conceived of, reduced to practice, authored or otherwise developed by RapidMiner or its agents. -2.5 Deliverables. Unless the applicable Order expressly provides otherwise, Licensee shall have a non-exclusive, non-transferable, non-sublicensable license to use each Deliverable solely for purposes appurtenant to, and within the scope of, the license granted to Licensee under Sections 2.1 and 2.2 of this Agreement. RapidMiner shall retain all other right, title and interest in and to all Deliverables. +2.3 Restrictions. The Software shall not be used for any purpose other than as expressly authorized by this Agreement. In particular, but without limitation, Licensee shall not, nor permit any third party to: (i) assign, sublicense, market, sell, lease, rent, distribute, convey or otherwise transfer or make the Software available to, or use the Software on behalf of, any third party; (ii) adapt, alter, modify, or translate the Software; (iii) create derivative works of the Software; (iv) reverse engineer, decompile, disassemble or otherwise attempt to reconstruct or obtain the source code to all or any portion of the Software for any purpose; (v) pledge as security or otherwise encumber the rights and licenses granted hereunder with respect to the Software; (vi) use the Software in any service bureau, time sharing, rental, or application service provider arrangement, as such terms are ordinarily understood within the software industry, or otherwise provide access to the Software to third parties; (vii) use the Software in any manner not in compliance with all applicable laws, regulations, and rules; (viii) use the Software, or cause the Software to be used, on any device not owned, operated, or controlled by Licensee; (ix) tamper with or modify any License Key, or otherwise attempt to use the Software outside of its prescribed Field of Use; (x) disclose benchmark results, competitive analyses, or any other information regarding the performance, design, functionality or features of the Software; or (xi) develop any product or service that competes with the Software or with RapidMiner. +2.4 Reservation of Rights. Nothing in this Agreement shall be deemed to grant Licensee, either directly or by implication, estoppel, or otherwise, any license or rights other than those expressly granted in Sections 2.1 and 2.2 of this Agreement. By virtue of this Agreement, Licensee acquires only the right to use the Software and does not acquire any other rights or ownership interests. RapidMiner reserves all rights to the Software not expressly granted to Licensee under this Agreement. RapidMiner shall retain all right, title, and interest in and to the Software, the Services, the Deliverables, the Confidential Information, and any improvements to any of the foregoing (including without limitation any improvements suggested by Licensee or by Licensee’s usage of the Software), as well as any other invention, modification, discovery, design, development, improvement, process, algorithm, software, documentation, formula, data, technique, know-how or other invention, innovation or work of authorship, or any interest therein (whether or not patentable or registrable under copyright or similar statutes or subject to analogous protection) discovered, conceived of, reduced to practice, authored or otherwise developed by RapidMiner or its agents. 3. Support and Services. 3.1 Support. Licensee shall be eligible to receive Support for the Software during any Support terms for which Licensee has subscribed to Support. 3.2 Other Services. Licensee may contract separately with RapidMiner to provide consulting, training, or other forms of professional services. +3.3 Dependencies. Licensee acknowledges that RapidMiner’s performance of the Services is dependent in part on Licensee’s cooperation and assistance. Accordingly, Licensee will provide RapidMiner, on a timely basis and at no expense to RapidMiner, with all items and assistance reasonably necessary to perform the Services, including without limitation any technical data, documentation, test data, sample output, or other information and resources of Licensee required by RapidMiner for the performance of the Services (the “Licensee Inputs”). Licensee shall be responsible for, and assumes the risk of any problems resulting from, the content, accuracy, completeness and consistency of any Licensee Inputs. Any dates or time periods applicable to RapidMiner’s performance hereunder shall be appropriately and equitably extended to account for any delays resulting from any failure by the Licensee to comply with the foregoing obligations. +3.4 Deliverables. Unless the applicable Order expressly provides otherwise, Licensee shall have a paid-up, non-transferable, non-sublicensable right and license to use each Deliverable solely for Licensee’s internal use. RapidMiner shall retain all other right, title and interest in and to all Deliverables. -4. Confidential Information. -4.1 Ownership. During the performance of this Agreement, Licensee will have access to certain Confidential Information of RapidMiner or Confidential Information of third parties that RapidMiner is required to maintain as confidential. All items of Confidential Information are proprietary to RapidMiner, and shall remain the sole property of RapidMiner. -4.2 Confidentiality Obligations. Licensee agrees (i) to use the Confidential Information only for the purposes described herein; (ii) that it will not reproduce the Confidential Information other than as permitted herein and will hold in confidence and protect such Confidential Information from dissemination to, and use by, any third party other than in accordance with clause "(iv)" below; (iii) that it will not create any derivative work from Confidential Information in violation of this Agreement or RapidMiner's copyrights; (iv) to restrict access to the Confidential Information to such of Licensee's personnel, agents, and/or consultants and contractors, if any, who have a need to have access and who have been advised of the obligation of confidentiality and have agreed in writing to treat such information in accordance with the terms of this Agreement; and (v) to return or destroy all Confidential Information in its possession upon termination or expiration of this Agreement. +4. Fees and Expenses +4.1 General. The provisions of this Section 4 shall not apply to any copies of the Software which (a) RapidMiner has provided to Licensee free of charge or (b) Licensee has previously paid for in full. +4.2 Fees. In consideration for the licenses granted to Licensee, Licensee shall pay to RapidMiner, without offset or deduction, the fees set forth in the applicable Order. Unless otherwise provided in such Order, all such fees shall be due and payable within thirty (30) calendar days after an invoice is issued by RapidMiner. +4.3 Taxes. Licensee shall pay all import duties, levies or imposts, and all sales, use, value added, property, or other taxes of any nature, assessed upon or with respect to any products or services provided to Licensee by RapidMiner, which are imposed by any community of nations or any nation, or any political subdivision of any nation, but excluding United States taxes based on RapidMiner's net income. Licensee shall pay on or before their due dates all such taxes, fees, duties and charges which arise out of or in connection with this Agreement or any license or sublicense granted herein or any use of the Software. In the event that RapidMiner is required at any time to pay any such tax, fee, duty or charge, Licensee shall promptly reimburse RapidMiner therefor. If Licensee is required by law to make any deduction or to withhold from any sum payable to RapidMiner by Licensee hereunder, then the sum payable by Licensee upon which the deduction or withholding is based shall be increased to the extent necessary to ensure that, after all deduction and withholding, RapidMiner receives and retains, free from liability for any deduction or withholding, a net amount equal to the amount RapidMiner would have received and retained in the absence of such required deduction or withholding. +4.4 Payment Terms. The fees for the Software shall be invoiced by RapidMiner per the terms of this Agreement or as otherwise set forth in the Order. Licensee's payments shall be past due thirty (30) days after the invoice date. Interest at the rate of twelve percent (12%) per annum (or, if lower, the maximum rate permitted by applicable law) shall accrue on any amount not paid to RapidMiner by Licensee when due under this Agreement. If Licensee fails to make any payment when due, RapidMiner may suspend delivery of any product or service until payment has been made in full. All costs of collection (including reasonable attorney fees) shall be paid by Licensee. All fees and other amounts paid or owed by Licensee under this Agreement are non-refundable and non-cancelable. All dollar amounts referred to in this Agreement are in United States Dollars. +4.5 Audit. From time to time during the term of this Agreement, RapidMiner or its designated agent (the “Auditor”) may perform a review and audit of Licensee’s compliance with this Agreement, including without limitation whether Licensee’s use of the Software is in compliance with any applicable Field of Use restrictions (the “Audit”). The Auditor shall comply with all reasonable confidentiality and security requirements that Licensee may impose upon the Audit. Without limiting any other rights or remedies that RapidMiner may have, in the event that the Audit discloses any material non-compliance with the terms of this Agreement, Licensee shall pay RapidMiner (a) the reasonable fees, costs and expenses incurred by RapidMiner in connection with the Audit and (b) one hundred and fifty percent (150%) of the amount of any fees that were due from Licensee but not paid. -5. Warranties. -THE SOFTWARE IS PROVIDED AS-IS AND WHERE-IS. RAPIDMINER MAKES NO WARRANTIES WHATSOEVER WITH RESPECT TO THE SOFTWARE, THE SERVICES OR THIS AGREEMENT, AND RAPIDMINER HEREBY DISCLAIMS ANY AND ALL WARRANTIES, CONDITIONS OR REPRESENTATIONS (WHETHER EXPRESS OR IMPLIED, ORAL OR WRITTEN), INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OF TITLE, NON-INFRINGEMENT, INFORMATION, MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE (WHETHER OR NOT RAPIDMINER KNOWS OR HAS REASON TO KNOW OF SUCH PURPOSE), WHETHER ARISING BY LAW, CUSTOM, USAGE IN THE TRADE OR BY COURSE OF DEALING. RAPIDMINER SPECIFICALLY DISCLAIMS ANY WARRANTY THAT THE OPERATION OF THE SOFTWARE WILL BE UNINTERRUPTED OR ERROR FREE. IN ADDITION, RAPIDMINER EXPRESSLY DISCLAIMS ANY WARRANTIES TO ANY PERSON OTHER THAN LICENSEE. +5. Confidential Information. +5.1 Ownership. During the performance of this Agreement, Licensee will have access to certain Confidential Information of RapidMiner or Confidential Information of third parties that RapidMiner is required to maintain as confidential. All items of Confidential Information are proprietary to RapidMiner, and shall remain the sole property of RapidMiner. +5.2 Confidentiality Obligations. Licensee agrees (i) to use the Confidential Information only for the purposes described herein; (ii) that it will not reproduce the Confidential Information other than as permitted herein and will hold in confidence and protect such Confidential Information from dissemination to, and use by, any third party other than in accordance with clause “(iv)” below; (iii) that it will not create any derivative work from Confidential Information in violation of this Agreement or RapidMiner’s copyrights; (iv) to restrict access to the Confidential Information to such of Licensee’s personnel, agents, and/or consultants and contractors, if any, who have a need to have access and who have been advised of the obligation of confidentiality and have agreed in writing to treat such information in accordance with the terms of this Agreement; and (v) to return or destroy all Confidential Information in its possession upon termination or expiration of this Agreement. -6. Indemnity. +6. Warranties. +THE SOFTWARE IS PROVIDED AS-IS AND WHERE-IS. RAPIDMINER MAKES NO WARRANTIES WHATSOEVER WITH RESPECT TO THE SOFTWARE, THE SERVICES OR THIS AGREEMENT, AND RAPIDMINER HEREBY DISCLAIMS ANY AND ALL WARRANTIES, CONDITIONS OR REPRESENTATIONS (WHETHER EXPRESS OR IMPLIED, ORAL OR WRITTEN), INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OF TITLE, NON-INFRINGEMENT, INFORMATION, MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE (WHETHER OR NOT RAPIDMINER KNOWS OR HAS REASON TO KNOW OF SUCH PURPOSE), WHETHER ARISING BY LAW, CUSTOM, USAGE IN THE TRADE OR BY COURSE OF DEALING. RAPIDMINER SPECIFICALLY DISCLAIMS ANY WARRANTY THAT THE OPERATION OF THE SOFTWARE WILL BE UNINTERRUPTED OR ERROR FREE. IN ADDITION, RAPIDMINER EXPRESSLY DISCLAIMS ANY WARRANTIES TO ANY PERSON OTHER THAN LICENSEE. + +7. Indemnity. Licensee shall indemnify and hold harmless RapidMiner and its officers, directors, employees, agents, representatives, subsidiaries and affiliates, from and against any and all claims, demands, damages, liabilities, losses and expenses (including without limitation all attorneys fees, costs and expenses) of any kind whatsoever, arising directly or indirectly out of any representation, action or omission by Licensee that is inconsistent with the terms of this Agreement. -7. Limitation of Liability. -7.1 Maximum Liability. RapidMiner's cumulative liability, whether in contract, tort, or otherwise, arising out of or in connection with the Software, the Services or this Agreement, shall not exceed the amount of any fees paid to RapidMiner by Licensee pursuant to this Agreement during the twelve (12) months preceding such claim. -7.2 Exclusion of Non-Direct Damages. IN NO EVENT SHALL RAPIDMINER BE LIABLE FOR SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY OR TORT DAMAGES (INCLUDING, WITHOUT LIMITATION, ANY DAMAGES RESULTING FROM LOSS OF USE, LOSS OF DATA, LOSS OF PROFITS OR LOSS OF BUSINESS) ARISING OUT OF OR IN CONNECTION WITH THE SOFTWARE, THE SERVICES OR THIS AGREEMENT, WHETHER OR NOT RAPIDMINER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. -7.3 Timely Claims. No action arising out of or in connection with the Software, the Services or this Agreement, regardless of form, may be brought by Licensee more than one (1) year after the occurrence of the events which gave rise to the cause of action. -7.4 Extensions. In no event shall RapidMiner have any liability, whether in contract, tort, warranty or otherwise, for any claims, demands, damages, liabilities, losses and expenses of any kind whatsoever, resulting directly or indirectly from Licensee's use of any Third Party Extension. -7.5 Third-Party Liability. Portions of the Software may be derived from or incorporate third-party software and no such third party warrants the Software, assumes any liability in connection with the Software or undertakes to furnish any support or information relating to the Software. All such third parties are intended third-party beneficiaries of this Agreement. -7.6 General. The limitations contained in this Section 7 shall survive the termination of this Agreement and apply notwithstanding any failure of essential purpose or any invalidity of the limited remedies provided for in this Agreement. +8. Limitation of Liability. +8.1 Maximum Liability. RapidMiner's cumulative liability, whether in contract, tort, or otherwise, arising out of or in connection with the Software, the Services or this Agreement, shall not exceed the amount of any fees paid to RapidMiner by Licensee pursuant to this Agreement during the twelve (12) months preceding such claim. +8.2 Exclusion of Non-Direct Damages. IN NO EVENT SHALL RAPIDMINER BE LIABLE FOR SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY OR TORT DAMAGES (INCLUDING, WITHOUT LIMITATION, ANY DAMAGES RESULTING FROM LOSS OF USE, LOSS OF DATA, LOSS OF PROFITS OR LOSS OF BUSINESS) ARISING OUT OF OR IN CONNECTION WITH THE SOFTWARE, THE SERVICES OR THIS AGREEMENT, WHETHER OR NOT RAPIDMINER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +8.3 Timely Claims. No action arising out of or in connection with the Software, the Services or this Agreement, regardless of form, may be brought by Licensee more than one (1) year after the occurrence of the events which gave rise to the cause of action. +8.4 Extensions. In no event shall RapidMiner have any liability, whether in contract, tort, warranty or otherwise, for any claims, demands, damages, liabilities, losses and expenses of any kind whatsoever, resulting directly or indirectly from Licensee’s use of any Third Party Extension. +8.5 Third-Party Liability. Portions of the Software may be derived from or incorporate third-party software and no such third party warrants the Software, assumes any liability in connection with the Software or undertakes to furnish any support or information relating to the Software. All such third parties are intended third-party beneficiaries of this Agreement. +8.6 General. The limitations contained in this Section 8 shall survive the termination of this Agreement and apply notwithstanding any failure of essential purpose or any invalidity of the limited remedies provided for in this Agreement. -8. Term and Termination. -8.1 Term. The term of this Agreement shall commence on the Effective Date and shall, unless earlier terminated as contemplated below, continue in force and effect for the duration of the License Term. -8.2 Termination. At any time during the term of this Agreement, each party shall have the right to terminate this Agreement immediately and without further obligation or liability hereunder if the other party breaches any material term of this Agreement and fails to remedy such breach within thirty (30) days after written notice by the non-breaching party of such breach. -8.3 Rights and Obligations Upon Termination. Upon expiration or termination of this Agreement, all rights granted to Licensee hereunder shall cease and Licensee shall promptly purge all copies of the Software or any portion thereof from all machines and/or other computer storage devices or media on which Licensee has placed such Software. The provisions of Sections 2.4, 4, 6, 7, 8 and 9 of this Agreement shall survive any termination or expiration of this Agreement. +9. Term and Termination. +9.1 Term. The term of this Agreement shall commence on the Effective Date and shall, unless earlier terminated as contemplated below, continue in force and effect for the duration of the License Term. +9.2 Termination. At any time during the term of this Agreement, each party shall have the right to terminate this Agreement immediately and without further obligation or liability hereunder if the other party breaches any material term of this Agreement and fails to remedy such breach within thirty (30) days after written notice by the non-breaching party of such breach. +9.3 Rights and Obligations Upon Termination. Upon expiration or termination of this Agreement, all rights granted to Licensee hereunder shall cease and Licensee shall promptly purge all copies of the Software or any portion thereof from all machines and/or other computer storage devices or media on which Licensee has placed such Software. The provisions of Sections 2.4, 4.4, 4.5, 5, 7, 8, 9 and 10 of this Agreement shall survive any termination or expiration of this Agreement. -9. General. -9.1 Entire Agreement. This Agreement and the applicable Order sets forth the entire agreement and understanding between the parties with respect to the subject matter hereof and supersedes and merges all prior oral and written agreements, discussions and understandings between the parties with respect to the subject matter hereof, and neither of the parties shall be bound by any conditions, inducements or representations other than as expressly provided for in this Agreement; provided, however, that this Agreement shall not supersede the terms of any signed license agreement between Licensee and RapidMiner relating to the Software (a "Signed Agreement"). In the event of any conflict between this Agreement and a Signed Agreement, the terms of the Signed Agreement shall prevail. In the event of any conflict between this Agreement and an Order, the terms of the Order shall prevail. -9.2 Government End-Users. Each of the components that constitute Software and its related documentation is a "commercial item" as that term is defined at 48 C.F.R. 2.101, consisting of "commercial computer software" and "commercial computer software documentation" as such terms are used in 48 C.F.R. 12.212. Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4, all U.S. Government end users acquire the components of Software and any documentation provided with Software with only those rights set forth in this Agreement. -9.3 Privacy Policy. Licensee acknowledges and agrees that the Software may collect certain usage statistics and other information concerning Licensee's use of the Software. Any such information shall be subject to RapidMiner's then-current privacy policy, available at https://rapidminer.com/privacy-policy/. -9.4 Audit. From time to time during the term of this Agreement, RapidMiner or its designated agent (the "Auditor") may perform a review and audit of Licensee's compliance with this Agreement, including without limitation whether Licensee's use of the Software is in compliance with any applicable Field of Use restrictions (the "Audit"). The Auditor shall comply with all reasonable confidentiality and security requirements that Licensee may impose upon the Audit. Without limiting any other rights or remedies that RapidMiner may have, in the event that the Audit discloses any material non-compliance with the terms of this Agreement, Licensee shall pay RapidMiner (a) the reasonable fees, costs and expenses incurred by RapidMiner in connection with the Audit and (b) one hundred and fifty percent (150%) of the amount of any fees that were due from Licensee but not paid. -9.5 Compliance with Laws. Licensee shall comply with all applicable laws, regulations, rules, orders and other requirements, now or hereafter in effect, of any applicable governmental authority, in connection with this Agreement, including without limitation, compliance with all export control laws and regulations of the United States and the European Union. Licensee shall, at its sole cost and expense, obtain and maintain in effect all permits, licenses and other consents necessary to the conduct of its activities hereunder. -9.6 Independent Contractors. The parties to this Agreement are independent parties and nothing herein shall be construed as creating an employment relationship between the parties. Neither party is an agent or representative of the other party and neither party shall have any right, power or authority to enter into any agreement for or on behalf of, or incur any obligation or liability, or otherwise bind, the other party. The Agreement shall not be interpreted or construed to create an association, agency, joint venture or partnership between the parties or to impose any liability attributable to such a relationship upon either party. -9.7 Notices. All notices under this Agreement will be in writing, and will be deemed given when personally delivered, when sent by confirmed fax, prepaid certified or registered U.S. mail, return receipt requested, or a recognized delivery service to the address of the party to the address set forth above, or to such other address as such party last provided to the other by written notice. -9.8 Amendments; Modifications. This Agreement may not be amended or modified except in a writing duly executed by authorized representatives of both parties. -9.9 Assignment. This Agreement shall inure to the benefit of and be binding upon the parties hereto, their successors and permitted assigns. Licensee may not assign this Agreement without the prior written consent of RapidMiner. RapidMiner may assign any of its rights or delegate any of its duties under this Agreement to any person or entity. -9.10 Severability. If any provision of this Agreement is invalid or unenforceable for any reason in any jurisdiction, such provision shall be construed to have been adjusted to the minimum extent necessary to cure such invalidity or unenforceability. The invalidity or unenforceability of one or more of the provisions contained in this Agreement shall not have the effect of rendering any such provision invalid or unenforceable in any other case, circumstance or jurisdiction, or of rendering any other provisions of this Agreement invalid or unenforceable whatsoever. -9.11 Waiver. No waiver of any provision of this Agreement, or any rights or obligations of either party under this Agreement, shall be effective, except pursuant to a written instrument signed by the party or parties waiving compliance, and any such waiver shall be effective only in the specific instance and for the specific purpose stated in such writing. The failure of either party to require the performance of any term of this Agreement or the waiver of either party of any breach under this Agreement shall not operate or be construed as a waiver of any other provision hereof, nor shall it be construed as a waiver of any subsequent breach by the other party hereto. -9.12 Governing Law. The laws of the Commonwealth of Massachusetts shall govern this Agreement, without giving effect to applicable conflict of laws provisions or to the United Nations Convention on Contracts for the International Sale of Goods. -9.13 Jurisdiction. The parties agree that the jurisdiction and venue of any action with respect to this Agreement shall be in a court of competent subject matter jurisdiction located in the Commonwealth of Massachusetts, and each of the parties hereby agrees to submit itself to the exclusive jurisdiction and venue of such courts for the purpose of any such action. -9.14 Equitable Relief. The covenants of Licensee in Section 2 and Section 4 hereof are of a special and unique character, and Licensee acknowledges that money damages alone will not reasonably or adequately compensate RapidMiner for any breach of such covenants. Therefore, RapidMiner and Licensee expressly agree that in the event of the breach or threatened breach of any such covenants, in addition to other rights or remedies which RapidMiner may have, at law, in equity, or otherwise, RapidMiner shall be entitled to injunctive or other equitable relief compelling specific performance of, and other compliance with, the terms of such Sections. -9.15 Force Majeure. RapidMiner shall not be liable for any delay or failure in performance hereunder caused by reason of any occurrence or contingency beyond its reasonable control, including but not limited to, acts of God, war, riot, civil disturbance, acts of any civil or military authority, judicial action, terrorist act, fire, flood, earthquake, strike, delays in transportation, unavailability or shortages of labor, materials or equipment, failure or delays in delivery of vendors and suppliers, interruption or failure of telecommunication or digital transmission links, Internet disruptions, common carrier interruptions, breakdown in facilities, power failure or other accidents or unforeseen circumstances. The obligations and rights so excused shall be extended on a day to day basis for the period of time equal to that of the underlying cause of the delay. -9.16 Remedies. No remedy referred to in this Agreement is intended to be exclusive, but each shall be cumulative and in addition to any other remedy referred to herein or otherwise available at law, in equity or otherwise. -9.17 Construction. The section and paragraph headings used in this Agreement are inserted for convenience only and shall not affect the meaning or interpretation of this Agreement. All words used in this Agreement will be construed to be of such gender or number as the circumstances require. Unless otherwise expressly provided, the word "including" does not limit the preceding words or terms. Both parties acknowledge that they have been represented by counsel in the negotiation of this Agreement, and hereby waive any canon of construction that would require any portion of this Agreement to be construed against the drafter thereof. +10. General. +10.1 Entire Agreement. This Agreement and the applicable Order sets forth the entire agreement and understanding between the parties with respect to the subject matter hereof and supersedes and merges all prior oral and written agreements, discussions and understandings between the parties with respect to the subject matter hereof, and neither of the parties shall be bound by any conditions, inducements or representations other than as expressly provided for in this Agreement; provided, however, that this Agreement shall not supersede the terms of any signed license agreement between Licensee and RapidMiner relating to the Software (a "Signed Agreement"). In the event of any conflict between this Agreement and a Signed Agreement, the terms of the Signed Agreement shall prevail. In the event of any conflict between this Agreement and an Order, the terms of the Order shall prevail. +10.2 Government End-Users. Each of the components that constitute Software and its related documentation is a “commercial item” as that term is defined at 48 C.F.R. 2.101, consisting of “commercial computer software” and “commercial computer software documentation” as such terms are used in 48 C.F.R. 12.212. Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4, all U.S. Government end users acquire the components of Software and any documentation provided with Software with only those rights set forth in this Agreement. +10.3 Privacy Policy. Licensee acknowledges and agrees that the Software may collect certain usage statistics and other information concerning Licensee’s use of the Software. Any such information shall be subject to RapidMiner’s then-current privacy policy, available at https://rapidminer.com/privacy-policy/. +10.4 Compliance with Laws. Licensee shall comply with all applicable laws, regulations, rules, orders and other requirements, now or hereafter in effect, of any applicable governmental authority, in connection with this Agreement, including without limitation, compliance with all export control laws and regulations of the United States and the European Union. Licensee shall, at its sole cost and expense, obtain and maintain in effect all permits, licenses and other consents necessary to the conduct of its activities hereunder. +10.5 Independent Contractors. The parties to this Agreement are independent parties and nothing herein shall be construed as creating an employment relationship between the parties. Neither party is an agent or representative of the other party and neither party shall have any right, power or authority to enter into any agreement for or on behalf of, or incur any obligation or liability, or otherwise bind, the other party. The Agreement shall not be interpreted or construed to create an association, agency, joint venture or partnership between the parties or to impose any liability attributable to such a relationship upon either party. +10.6 Notices. All notices under this Agreement will be in writing, and will be deemed given when personally delivered, when sent by confirmed fax, prepaid certified or registered U.S. mail, return receipt requested, or a recognized delivery service to the address of the party to the address set forth above, or to such other address as such party last provided to the other by written notice. +10.7 Amendments; Modifications. This Agreement may not be amended or modified except in a writing duly executed by authorized representatives of both parties. +10.8 Assignment. This Agreement shall inure to the benefit of and be binding upon the parties hereto, their successors and permitted assigns. Licensee may not assign this Agreement without the prior written consent of RapidMiner. RapidMiner may assign any of its rights or delegate any of its duties under this Agreement to any person or entity. +10.9 Severability. If any provision of this Agreement is invalid or unenforceable for any reason in any jurisdiction, such provision shall be construed to have been adjusted to the minimum extent necessary to cure such invalidity or unenforceability. The invalidity or unenforceability of one or more of the provisions contained in this Agreement shall not have the effect of rendering any such provision invalid or unenforceable in any other case, circumstance or jurisdiction, or of rendering any other provisions of this Agreement invalid or unenforceable whatsoever. +10.10 Waiver. No waiver of any provision of this Agreement, or any rights or obligations of either party under this Agreement, shall be effective, except pursuant to a written instrument signed by the party or parties waiving compliance, and any such waiver shall be effective only in the specific instance and for the specific purpose stated in such writing. The failure of either party to require the performance of any term of this Agreement or the waiver of either party of any breach under this Agreement shall not operate or be construed as a waiver of any other provision hereof, nor shall it be construed as a waiver of any subsequent breach by the other party hereto. +10.11 Governing Law. The laws of the Commonwealth of Massachusetts shall govern this Agreement, without giving effect to applicable conflict of laws provisions or to the United Nations Convention on Contracts for the International Sale of Goods. +10.12 Jurisdiction. The parties agree that the jurisdiction and venue of any action with respect to this Agreement shall be in a court of competent subject matter jurisdiction located in the Commonwealth of Massachusetts, and each of the parties hereby agrees to submit itself to the exclusive jurisdiction and venue of such courts for the purpose of any such action. +10.13 Equitable Relief. The covenants of Licensee in Section 2 and Section 4 hereof are of a special and unique character, and Licensee acknowledges that money damages alone will not reasonably or adequately compensate RapidMiner for any breach of such covenants. Therefore, RapidMiner and Licensee expressly agree that in the event of the breach or threatened breach of any such covenants, in addition to other rights or remedies which RapidMiner may have, at law, in equity, or otherwise, RapidMiner shall be entitled to injunctive or other equitable relief compelling specific performance of, and other compliance with, the terms of such Sections. +10.14 Self Help. RapidMiner reserves the right to withhold access to the Software or the Services, or utilize other technical means of “self help”, in the event that Licensee breaches this Agreement. +10.15 Force Majeure. RapidMiner shall not be liable for any delay or failure in performance hereunder caused by reason of any occurrence or contingency beyond its reasonable control, including but not limited to, acts of God, war, riot, civil disturbance, acts of any civil or military authority, judicial action, terrorist act, fire, flood, earthquake, strike, delays in transportation, unavailability or shortages of labor, materials or equipment, failure or delays in delivery of vendors and suppliers, interruption or failure of telecommunication or digital transmission links, Internet disruptions, common carrier interruptions, breakdown in facilities, power failure or other accidents or unforeseen circumstances. The obligations and rights so excused shall be extended on a day to day basis for the period of time equal to that of the underlying cause of the delay. +10.16 Remedies. No remedy referred to in this Agreement is intended to be exclusive, but each shall be cumulative and in addition to any other remedy referred to herein or otherwise available at law, in equity or otherwise. +10.17 Construction. The section and paragraph headings used in this Agreement are inserted for convenience only and shall not affect the meaning or interpretation of this Agreement. All words used in this Agreement will be construed to be of such gender or number as the circumstances require. Unless otherwise expressly provided, the word “including” does not limit the preceding words or terms. Both parties acknowledge that they have been represented by counsel in the negotiation of this Agreement, and hereby waive any canon of construction that would require any portion of this Agreement to be construed against the drafter thereof. -======================================================================================= +================================================ *** YOU ACKNOWLEDGE THAT YOU HAVE READ THIS AGREEMENT, UNDERSTAND IT, AND AGREE TO BE BOUND BY ITS TERMS AND CONDITIONS. *** \ No newline at end of file diff --git a/src/main/resources/com/rapidminer/resources/groups.properties b/src/main/resources/com/rapidminer/resources/groups.properties index c646d98e7..d5eccfb30 100644 --- a/src/main/resources/com/rapidminer/resources/groups.properties +++ b/src/main/resources/com/rapidminer/resources/groups.properties @@ -40,6 +40,7 @@ io.com.rapidminer.operator.visualization.dependencies.NumericalMatrix.color = #9 io.com.rapidminer.operator.visualization.LiftParetoChart.color = #99c8cc io.com.rapidminer.operator.visualization.DataStatistics.color = #99c8cc io.com.rapidminer.operator.visualization.ROCComparison.color = #99c8cc +io.com.rapidminer.connection.ConnectionInformationContainerIOObject.color = #ffa500 # red group.utility.color = #fde4d7 diff --git a/src/main/resources/com/rapidminer/resources/i18n/Errors.properties b/src/main/resources/com/rapidminer/resources/i18n/Errors.properties index 3b9e46382..a0ac151a3 100644 --- a/src/main/resources/com/rapidminer/resources/i18n/Errors.properties +++ b/src/main/resources/com/rapidminer/resources/i18n/Errors.properties @@ -29,14 +29,14 @@ metadata.quickfix.switch_to_shuffled_sampling = Switch sampling to shuffled. metadata.quickfix.relativize_repository_location = Resolve repository location with respect to process. -metadata.error.port_connected_but_parameter_not_set = Data at port ''{0}'' will not be used because Parameter ''{1}'' is set to ''{2}''. +metadata.error.port_connected_but_parameter_not_set = Data at port ''{0}'' will not be used because Parameter ''{1}'' is set to ''{2}''. metadata.error.attribute_missing = The attribute ''{0}'' might be missing in the input example set. metadata.error.attribute_contains_negative_values = The attribute ''{0}'' contains negative values which is not supported by {1}. metadata.error.exception_checking_precondition = Error checking precondition: {0} -metadata.error.input_missing = Mandatory input missing at port {0}. +metadata.error.input_missing = Mandatory input missing at port {0}. metadata.error.expected = Expected {0} but received {1}. metadata.error.metadata_underspecified = Meta data is underspecified. Cannot check precondition. metadata.error.special_missing = Input example set must have special attribute ''{0}''. @@ -46,7 +46,7 @@ metadata.error.unkown_attribute = Cannot check if input example set contains metadata.error.special_attribute_has_wrong_type = The attribute ''{0}'' of role {1} must be of type {2}. metadata.error.attribute_has_wrong_type = The attribute ''{0}'' must be of type {1}. metadata.error.attribute_must_have_role = The attribute ''{0}'' needs to fulfill the role {1}. - + metadata.error.already_contains_attribute = The input example set already contains an attribute ''{0}''. metadata.error.already_contains_role = The input example set already contains an attribute of the specified role {0}. It will be removed from example set. metadata.error.missing_id = The input example set needs to contain an ID attribute. @@ -68,14 +68,14 @@ metadata.error.exampleset.must_contain_numerical_attribute = The example set mus metadata.error.exampleset.must_contain_text_attribute = The example set must contain at least one text attribute. metadata.error.exampleset.need_more_examples = The exampleset must contain at least {0} examples. metadata.error.exampleset.parameters.need_more_attributes = The exampleset must contain at least {0} attributes with parameter "{1}" set to "{2}". -metadata.error.exampleset.parameters.need_more_examples = The exampleset must contain at least {0} examples with parameter "{1}" set to "{2}". -metadata.error.exampleset.parameters.attribute_must_be_numerical = The attribute "{0}" must be numerical with parameter "{1}" set to "{2}". +metadata.error.exampleset.parameters.need_more_examples = The exampleset must contain at least {0} examples with parameter "{1}" set to "{2}". +metadata.error.exampleset.parameters.attribute_must_be_numerical = The attribute "{0}" must be numerical with parameter "{1}" set to "{2}". metadata.error.exampleset.needs_prediction = The input example set has to contain a prediction attribute. metadata.error.exampleset.parameter_value_exceeds_exampleset_size = The parameter {0} indexes an example, but the value {1} exceeds the example set size. metadata.error.exampleset.window_exceeds_exampleset_size = The parameter {0} specifies a window size, but the value {1} exceeds the example set size. metadata.error.exampleset.window_exceeds_attributeset_size = The parameter {0} specifies a window size, but the value {1} exceeds the number of attributes. metadata.error.parameters.incompatible_for_delivering = Cannot deliver {0} with the current parameter settings. -metadata.error.parameters.setting_incompatible_for_delivering = Cannot deliver {0} with parameter "{1}" set to "{2}". +metadata.error.parameters.setting_incompatible_for_delivering = Cannot deliver {0} with parameter "{1}" set to "{2}". metadata.error.parameters.cannot_handle = Cannot handle {0} with parameter "{1}" set to "{2}". metadata.error.incompatible_ioobjects = Input objects in collection are incompatible: {0} and {1}. metadata.error.not_defined_on_nominal = The {0} is not defined on nominal attributes. @@ -89,9 +89,9 @@ metadata.error.included_process_recursiv_call = Cannot calculate meta data for r metadata.error.attribute_selection_empty = Attribute filter does not match any attributes. metadata.error.cannot_parse_date = Cannot parse date: {0} metadata.error.cannot_parse_expression = Cannot parse expression -metadata.error.measures.nominal = The example set contains non nominal attributes. The distance measure {0} is defined only for nominal attributes. +metadata.error.measures.nominal = The example set contains non nominal attributes. The distance measure {0} is defined only for nominal attributes. metadata.error.measures.numerical = The example set contains non numerical attributes. The distance measure {0} is defined only for numerical attributes. -metadata.error.missing_value_replenishment.value_type_not_supported_by_replenishment = The replenishment "{0}" does not support the value types of the following selected attributes: {1}. +metadata.error.missing_value_replenishment.value_type_not_supported_by_replenishment = The replenishment "{0}" does not support the value types of the following selected attributes: {1}. metadata.error.aggregation.incompatible_value_type = The value type of the attribute {0} is not compatible with the aggregation function {1}. It requires an attribute of type {3} but is {2}. metadata.error.aggregation.incompatible_value_type_multiple = The value type of the attribute {0} is not compatible with the aggregation function {1}. It requires an attribute of type {3} or {4} but is {2}. @@ -187,6 +187,10 @@ process.error.no_send_mail_command = No command for sendmail set. Sending mails process.error.attributes_type_mismatch = The attribute ''{0}'' from input port {1} must be of the same type as ''{2}'' from input port {3}. process.error.parameter.required_missing = The required parameter ''{0}'' is not set. +process.error.io.dir_creation_fail = Failed to create directory ''{0}''. Reason: {1} +process.error.io.invalid_filepath = File path ''{0}'' is invalid +process.error.io.writing_fail = Writing to file ''{0}'' failed. Reason: {1}. Check file permissions. + repository.illegal_entry_name = Entry ''{0}'' at location ''{1}'' contains characters which cannot be stored on some filesystems and are therefore illegal. repository.repository_folder_already_exists = Repository folder ''{0}'' already exists. repository.repository_entry_with_same_name_already_exists = Repository already contains an entry with the name ''{0}''. @@ -194,10 +198,17 @@ repository.repository_move_same_folder = The source and target location are iden repository.repository_move_into_subfolder = The target location is a subfolder of the source location, cannot move entry. repository.repository_copy_same_folder = The source and target location are identical, cannot copy entry. repository.repository_copy_into_subfolder = The target location is a subfolder of the source location, cannot copy entry. -repository.repository_creation_duplicate_location = The folder you selected points to an already existing repository. +repository.repository_creation_duplicate_location = The folder you selected points to the already existing repository {0}. repository.repository_creation_duplicate_alias = The alias you selected is already being used for an existing repository. repository.repository_creation_duplicate_url = Multiple repositories for the same URL are not allowed, cannot create repository. +repository.create_connection_folder = Only connections can be stored in the Connections folder. Please store in another folder. +repository.connection_create_outside = Connections can only be stored in the Connections folder. +repository.connection_folder_change = The special Connections folder cannot be moved, renamed or deleted. +repository.connection_folder_duplicate = Cannot create another special Connections folder. +repository.connection_folder_unknown = This repository cannot store connections. +repository.create_connections_failed = Failed to create connections folder for local repository {0}. + repository.password_input_canceled = Authentication was canceled by user. Please check your credentials. repository.error.check_connection.authentication_error = User or password wrong @@ -206,6 +217,7 @@ repository.error.check_connection.other_authentication_error = User, password or repository.error.check_connection.internal_server_error = Internal server error repository.error.check_connection.unkown_error = Unknown error +repository.error.check_connection.general_error = Error during connection: ''{0}'' repository.error.check_connection.no_user = User is empty repository.error.check_connection.no_password = Password is empty @@ -240,25 +252,25 @@ numeric_value_filter.and_combined_with_or = || and && are not allowed in one con numeric_value_filter.invalid_syntax = Invalid condition syntax numeric_value_filter.missing_relational_operator = Missing relational operator -expression_parser.unknown_function = The function ''{0}'' is unknown. -expression_parser.unknown_operator = The operator ''{0}'' is unknown. -expression_parser.unknown_attribute = The attribute ''{0}'' is unknown. -expression_parser.unknown_variable = The variable ''{0}'' is unknown. -expression_parser.unknown_scope = The macro ''{0}'' is unknown. -expression_parser.unknown_attribute_in_scope = The attribute ''{0}'' in the macro ''{1}'' is unknown. +expression_parser.unknown_function = The function ''{0}'' is unknown. +expression_parser.unknown_operator = The operator ''{0}'' is unknown. +expression_parser.unknown_attribute = The attribute ''{0}'' is unknown. +expression_parser.unknown_variable = The variable ''{0}'' is unknown. +expression_parser.unknown_scope = The macro ''{0}'' is unknown. +expression_parser.unknown_attribute_in_scope = The attribute ''{0}'' in the macro ''{1}'' is unknown. expression_parser.function_wrong_input = The function ''{0}'' must have {1} arguments but has {2}. expression_parser.function_wrong_input_two = The function ''{0}'' must have {1} or {2} arguments but has {3}. expression_parser.function_wrong_minimum_input = The function ''{0}'' must have more than {1} arguments but has {2}. -expression_parser.function_needs_same_type= ''{0}'' must have arguments of the same type. -expression_parser.function_wrong_type.argument= Argument ''{0}'' of function ''{1}'' must be of type ''{2}''. +expression_parser.function_needs_same_type= ''{0}'' must have arguments of the same type. +expression_parser.function_wrong_type.argument= Argument ''{0}'' of function ''{1}'' must be of type ''{2}''. expression_parser.function_wrong_type.argument_two= Argument ''{0}'' of function ''{1}'' must be of type ''{2}'' or ''{3}''. -expression_parser.function_wrong_type = ''{0}'' must have arguments of type ''{1}''. +expression_parser.function_wrong_type = ''{0}'' must have arguments of type ''{1}''. expression_parser.function_wrong_type_at = The function ''{0}'' must have an argument of type ''{1}'' as ''{2}'' argument. -expression_parser.function_wrong_type_two = ''{0}'' must have arguments of type ''{1}'' or ''{2}''. +expression_parser.function_wrong_type_two = ''{0}'' must have arguments of type ''{1}'' or ''{2}''. expression_parser.function_non_negative = The function ''{0}'' cannot handle negative arguments. expression_parser.function_non_negative_argument = The function ''{0}'' cannot handle negative values as {1} argument. expression_parser.function_non_empty_arguments= The function ''{0}'' cannot handle empty arguments. -expression_parser.function_missing_arguments= The mandatory argument ''{0}'' is missing for the function ''{1}''. +expression_parser.function_missing_arguments= The mandatory argument ''{0}'' is missing for the function ''{1}''. expression_parser.parameter_value_wrong_parameter = Unknown parameter as argument for ''{0}''. expression_parser.parameter_value_wrong_operator = Unknown operator as argument for ''{0}''. expression_parser.parameter_value_invalid_parameter = Invalid parameter ''{0}'' as argument for ''{1}''. @@ -269,6 +281,7 @@ expression_parser.invalid_argument.date_format = Invalid argument type for ''{0} expression_parser.parameter_value_too_big = The parameters ''{0}'' and ''{1}'' are too big for the given parameter ''{2}'' for the function ''{3}''. expression_parser.invalid_regex = The given regular expression ''{0}'' as argument of the function ''{1}'' is invalid. expression_parser.eval_failed = The function ''{0}'' failed to parse the subexpression ''{1}''. Cause: {2} +expression_parser.attribute_eval_failed = The function ''{0}'' failed to find the attribute specified by the subexpression evaluating to ''{1}''. expression_parser.eval.non_constant_single_argument = The function ''{0}'' has as argument an expression that is not constant, i.e. it depends on attribute values. Please specify the expected type of evaluating this expression as second argument. expression_parser.eval.type_not_matching = The evaluation of the first argument of the ''{0}'' function leads to a result of type ''{1}'' instead the expected type ''{2}'' as specified by the second argument. expression_parser.eval.invalid_type = The type ''{0}'' given as second argument to the ''{1}'' function is invalid. Please choose one of the type constants {2}. @@ -297,3 +310,20 @@ com.rapidminer.example.set.WrongPredictionCondition.missing_prediction = A wrong com.rapidminer.example.set.CorrectPredictionCondition.missing_label_and_prediction = A correct prediction filter requires an example set containing 'label' and 'prediction' attributes! com.rapidminer.example.set.CorrectPredictionCondition.missing_label = A correct prediction requires an example set containing a 'label' attribute! com.rapidminer.example.set.CorrectPredictionCondition.missing_prediction = A correct prediction requires an example set containing a 'prediction' attribute! + +access_control.no_internal_permission = Internal API, cannot be called by unauthorized sources. + +error.connections.value_missing = {0}: Value not set +generic_registry.missing_handler.core = Handler for {0} type ''{1}'' not found. Make sure you are using the correct version of RapidMiner. +generic_registry.missing_handler.extension = Handler for {0} type ''{1}'' not found. Maybe you are missing an extension. Check the marketplace for the namespace ''{2}''. +generic_registry.handler.type.connection = connection +generic_registry.handler.type.value_provider = value provider + +process.error.connection.repository_error = Repository error +process.error.connection.no_handler_registered = No handler could be found for the type ''{0]'' +process.error.connection.mismatched_type = Expected connection of type ''{0}'' but found ''{1}'' instead +metadata.error.connection.mismatched_type = Expected connection of type ''{0}'' but found ''{1}'' instead +process.error.connection.deprecated = The configurable mechanism is deprecated. Use the new repository based connections instead. + +repository.error.non_existent_entry = Entry ''{0}'' does not exist +repository.error.mismatched_entry_type = Entry ''{0}'' is not of type ''{1}'' but of type ''{2}'' diff --git a/src/main/resources/com/rapidminer/resources/i18n/GUI.properties b/src/main/resources/com/rapidminer/resources/i18n/GUI.properties index 1e4f1f876..fa2e2fc1a 100644 --- a/src/main/resources/com/rapidminer/resources/i18n/GUI.properties +++ b/src/main/resources/com/rapidminer/resources/i18n/GUI.properties @@ -175,7 +175,7 @@ gui.action.toolbar_resources.help_videos.tip = Jump-start your RapidMiner skil gui.action.toolbar_resources.help_forum.label = Visit Community (Web) gui.action.toolbar_resources.help_forum.mne = C gui.action.toolbar_resources.help_forum.icon = users.png -gui.action.toolbar_resources.help_forum.tip = Need help? Visit our community forum, with more than 350 000 members and active participation by our research team. +gui.action.toolbar_resources.help_forum.tip = Need help? Visit our community, with more than 500,000 members and active participation by our research team. gui.action.toolbar_resources.support.label = Support (Web) gui.action.toolbar_resources.support.mne = S @@ -262,6 +262,16 @@ gui.action.dont_ask_again.label = Don't ask again. gui.action.dont_ask_again.tip = Don't ask this question again the next time this action is performed. gui.action.dont_ask_again.mne = A +gui.action.connection.save_anyway.label = Save anyway +gui.action.connection.save_anyway.tip = Close this dialog and save the connection. +gui.action.connection.save_anyway.mne = S +gui.action.connection.save_anyway.icon = floppy_disk.png + +gui.action.connection.back_to_editing.label = Go back to editing +gui.action.connection.back_to_editing.tip = Close this dialog and go back to the edit view. +gui.action.connection.back_to_editing.mne = G +gui.action.connection.back_to_editing.icon = arrow_left.png + gui.label.numerical_matrix.not_enough_attributes.label = Warning: This matrix was not calculated since there were not enough valid attributes ############################ @@ -432,9 +442,37 @@ gui.dialog.confirm.error_in_copy_entry.title = Copy error gui.dialog.confirm.error_in_copy_entry.icon = error.png gui.dialog.confirm.error_in_copy_entry.message = Error on copying {0}

      Do you want to retry? +gui.dialog.error.error_modify_connections_folder.title = Connections folder cannot be modified +gui.dialog.error.error_modify_connections_folder.icon = error.png +gui.dialog.error.error_modify_connections_folder.message = The Connections folder of ''{0}'' cannot be moved, renamed, or deleted. + +gui.dialog.confirm.error_modify_connections_folder.title = Connections folder cannot be modified +gui.dialog.confirm.error_modify_connections_folder.icon = error.png +gui.dialog.confirm.error_modify_connections_folder.message = The Connections folder of ''{0}'' cannot be moved, renamed, or deleted. + +gui.dialog.error.error_copy_other_to_connections_folder.title = Only connections can be stored in Connections folder +gui.dialog.error.error_copy_other_to_connections_folder.icon = error.png +gui.dialog.error.error_copy_other_to_connections_folder.message = Cannot store {0}. You can only store connections in the Connections folder of a repository. + +gui.dialog.confirm.error_copy_other_to_connections_folder.title = Only connections can be stored in Connections folder +gui.dialog.confirm.error_copy_other_to_connections_folder.icon = error.png +gui.dialog.confirm.error_copy_other_to_connections_folder.message = Cannot store {0}. You can only store connections in the Connections folder of a repository. + +gui.dialog.error.error_connections_not_supported.title = Connections not supported +gui.dialog.error.error_connections_not_supported.icon = error.png +gui.dialog.error.error_connections_not_supported.message = Cannot store {0}. This repository does not support connections. + +gui.dialog.confirm.error_connections_not_supported.title = Connections not supported +gui.dialog.confirm.error_connections_not_supported.icon = error.png +gui.dialog.confirm.error_connections_not_supported.message = Cannot store {0}. This repository does not support connections. + +gui.dialog.confirm.error_copy_to_non_connections_folder.title = Connections can only be stored in Connections folder of a repository +gui.dialog.confirm.error_copy_to_non_connections_folder.icon = plug.png +gui.dialog.confirm.error_copy_to_non_connections_folder.message = {0}

      Do you want to store {1} in the Connections folder of ''{2}'' instead?
      Note that if a connection name is already taken, the copy will be renamed. + gui.dialog.confirm.error_in_copy_entry_with_cause.title = Copy error gui.dialog.confirm.error_in_copy_entry_with_cause.icon = error.png -gui.dialog.confirm.error_in_copy_entry_with_cause.message = Error on copying {0}

      Cause: {1}

      Do you want to retry? +gui.dialog.confirm.error_in_copy_entry_with_cause.message = Error on copying {0}

      Cause: {1}

      Do you want to retry? gui.dialog.confirm.error_in_delete_entry.title = Deletion error gui.dialog.confirm.error_in_delete_entry.icon = error.png @@ -469,7 +507,10 @@ gui.label.macro_selection_dialog.title = Insert macro gui.label.macro_selection_dialog.value.title = Insert as text gui.label.macro_selection_dialog.value.description = Use this option if the value of your macro is text, e.g. a country or a file name. gui.label.macro_selection_dialog.expression.title = Insert and evaluate -gui.label.macro_selection_dialog.expression.description = Use this option if the value of your macro is a number, an attribute name, or an expression. +gui.label.macro_selection_dialog.expression.description = Use this option if the value of your macro is a number or an expression. +gui.label.macro_selection_dialog.attribute.title = Insert as attribute +gui.label.macro_selection_dialog.attribute.description = Use this option if the value of your macro is the name of an attribute. + gui.action.text_dialog.enlarge.label = Enlarge gui.action.text_dialog.enlarge.icon = resize.png @@ -668,14 +709,14 @@ gui.label.onboarding.signup_info2.label = Already have an account? gui.label.onboarding.signup.info1.label = You\u2019ll use your RapidMiner gui.label.onboarding.signup.info2.label = Account to access: -gui.label.onboarding.signup.community_forum.label = the Community forum +gui.label.onboarding.signup.community_forum.label = the Community gui.label.onboarding.signup.community_forum.icon = 16/users_crowd.png gui.label.onboarding.signup.marketplace.label = the Extensions Marketplace gui.label.onboarding.signup.marketplace.icon = 16/registry.png -gui.label.onboarding.signup.cloud.label = free cloud storage -gui.label.onboarding.signup.cloud.icon = 16/server_cloud.png +gui.label.onboarding.signup.academy.label = the RapidMiner Academy +gui.label.onboarding.signup.academy.icon = 16/academy_video.png gui.label.onboarding.signup.news.label = product news and updates gui.label.onboarding.signup.news.icon = 16/megaphone.png @@ -788,7 +829,7 @@ gui.label.getting_started.documentation.description = If you wish to learn all t gui.label.getting_started.documentation.icon = document_orientation_portrait.png gui.label.getting_started.community.title = Visit Community -gui.label.getting_started.community.description = Need help? Visit our community forum, with more than 350 000 members and active participation by our research team. +gui.label.getting_started.community.description = Need help? Visit our community, with more than 500,000 members and active participation by our research team. gui.label.getting_started.community.icon = users.png gui.label.getting_started.trainingvideo.title = Explore RapidMiner Academy @@ -874,8 +915,6 @@ gui.dialog.error.incompatible_extensions.failed_to_query_market_place.icon = err gui.action.install_extension_dummy.label = Search for missing extension on Marketplace gui.label.dummy.parameter.install_extension = This operator requires an extension that is currently not installed. Click on this link in order to search for the missing extension on the Marketplace. -gui.dialog.error.dummy.marketplace_connection_error.title = Connection error -gui.dialog.error.dummy.marketplace_connection_error.message = Unable to connect to the RapidMiner Marketplace. # Install SAS Connector Extension from deprecated operator gui.action.install_extension_sas.label = Search for SAS Reader on Marketplace @@ -1073,21 +1112,23 @@ gui.license.rapidminer-studio.label = RapidMiner Studio gui.license.rapidminer-studio.starter.label = Free (uninitialized) gui.license.rapidminer-studio.free.label = Free -gui.license.rapidminer-studio.free_20k.label = Free +gui.license.rapidminer-studio.free_20k.label = Free gui.license.rapidminer-studio.free_30k.label = Free gui.license.rapidminer-studio.free_40k.label = Free gui.license.rapidminer-studio.free_50k.label = Free -gui.license.rapidminer-studio.small.label = Small -gui.license.rapidminer-studio.medium.label = Medium -gui.license.rapidminer-studio.non-commercial.label = Educational +gui.license.rapidminer-studio.professional.label = Professional gui.license.rapidminer-studio.trial.label = Trial -gui.license.rapidminer-studio.unlimited.label = Large +gui.license.rapidminer-studio.educational.label = Educational +gui.license.rapidminer-studio.enterprise.label = Enterprise gui.license.rapidminer-studio.developer.label = Developer # old editions +gui.license.rapidminer-studio.unlimited.label = Large +gui.license.rapidminer-studio.medium.label = Medium +gui.license.rapidminer-studio.small.label = Small +gui.license.rapidminer-studio.non-commercial.label = Educational gui.license.rapidminer-studio.community.label = Community gui.license.rapidminer-studio.academia.label = Academia -gui.license.rapidminer-studio.professional.label = Professional gui.license.rapidminer-studio.collaboration.label = Professional (Collaboration) gui.license.rapidminer-studio.compute.label = Professional (Computation) gui.license.rapidminer-studio.deployment.label = Professional (Deployment) @@ -1338,6 +1379,11 @@ gui.action.menu.connections.label = Connections gui.action.menu.connections.mne = C gui.action.menu.connections.tip = Manage connections. +gui.action.menu.legacy_connections.label = Legacy Connections +gui.action.menu.legacy_connections.mne = L +gui.action.menu.legacy_connections.icon = sign_warning.png +gui.action.menu.legacy_connections.tip = Legacy connections, will be removed in a future release. + gui.action.preferences.label = Preferences... gui.action.preferences.mne = P gui.action.preferences.icon = clipboard_check_edit.png @@ -1621,6 +1667,11 @@ gui.action.close_all_results.icon = selection_delete.png gui.action.close_all_results.mne = C gui.action.close_all_results.tip = Close all results which are currently open. +gui.action.close_all_results_except_current.label = Close all other results +gui.action.close_all_results_except_current.icon = windows_close.png +gui.action.close_all_results_except_current.mne = O +gui.action.close_all_results_except_current.tip = Close all results except this one. + ################################################################################################ # COMPONENTS ################################################################################################ @@ -1739,7 +1790,7 @@ gui.action.menu.quick_fixes.icon = first_aid.png gui.action.quickfix.connect_to.icon = plug2.png gui.action.quickfix.reconnect_to.icon = plug2.png gui.action.quickfix.add_compatible.icon = element_add.png -gui.action.quickfix.disconnect.icon = plug_delete.png +gui.action.quickfix.disconnect.icon = delete.png gui.action.quickfix.insert_id_tagging.icon = table_selection_column_add.png gui.action.quickfix.insert_model_applier.icon = lightbulb_on.png gui.action.quickfix.insert_discretization.icon = objects_transform.png @@ -1769,7 +1820,7 @@ gui.dialog.set_parameter.title = Set Parameter: {0} gui.dialog.set_parameter.icon = form_edit.png gui.dialog.input.cannot_connect.title = Cannot Connect -gui.dialog.input.cannot_connect.icon = plug.png +gui.dialog.input.cannot_connect.icon = plug2.png gui.dialog.input.cannot_connect.message = Cannot connect {0} to {1}: {2}. Please select an option how to proceed. gui.dialog.input.cannot_connect.reason.both_connected = both ports already connected gui.dialog.input.cannot_connect.reason.source_connected = source port is already connected to {0} @@ -1887,7 +1938,7 @@ gui.action.add_operator_now.icon = element_add.png # ProcessRenderer gui.action.connect_port_to_repository_location.label = Connect {0} to repository location. gui.action.connect_port_to_repository_location.mne = C -gui.action.connect_port_to_repository_location.icon = plug.png +gui.action.connect_port_to_repository_location.icon = plug2.png gui.action.connect_port_to_repository_location.tip = Connect {0} to a data entry stored in a repository. gui.action.select_all.label = Select all @@ -1976,7 +2027,7 @@ gui.action.delete_selected_connection.acc = DELETE gui.action.disconnect.label = Disconnect Port gui.action.disconnect.mne = D gui.action.disconnect.tip = Disconnect this port. -gui.action.disconnect.icon = plug_delete.png +gui.action.disconnect.icon = delete.png gui.action.delete_connection.label = Remove Connection gui.action.delete_connection.mne = R @@ -2138,6 +2189,7 @@ gui.action.no_matches_found.icon = gui.action.no_matches_found.tip = #RepositoryBrowser +gui.repository.db.legacy.label = Legacy gui.repository.db.icon = data_network.png gui.repository.local.icon = monitor.png gui.repository.resource.icon = folder.png @@ -2210,6 +2262,14 @@ gui.action.repository_delete_entry.acc = DELETE gui.action.repository_delete_entry.icon = folder_open_delete.png gui.action.repository_delete_entry.tip = Delete selected entry. +gui.action.repository_edit_connection.label = Edit +gui.action.repository_edit_connection.icon = pencil.png +gui.action.repository_edit_connection.tip = Edit selected connection. + +gui.action.repository_create_connection.label = Create Connection +gui.action.repository_create_connection.icon = plug_add.png +gui.action.repository_create_connection.tip = Create a new connection. + gui.action.repository_rename_entry.label = Rename gui.action.repository_rename_entry.mne = N gui.action.repository_rename_entry.acc = F2 @@ -2337,6 +2397,14 @@ gui.dialog.confirm.overwrite.title = Overwrite File gui.dialog.confirm.overwrite.icon = floppy_disk_warning.png gui.dialog.confirm.overwrite.message = Overwrite {0}? +gui.dialog.confirm.save_invalid_connection.title = Connection validation failed +gui.dialog.confirm.save_invalid_connection.message= {0} +gui.dialog.confirm.save_invalid_connection.icon = floppy_disk_warning.png + +gui.dialog.confirm.connection_unused_source.title = Unused configured source +gui.dialog.confirm.connection_unused_source.message= You are not using the configured source(s) {0} in this connection. The configuration will be lost on save. +gui.dialog.confirm.connection_unused_source.icon = floppy_disk_warning.png + gui.dialog.confirm.editor.search_replace.no_more_hits.title = String Not Found gui.dialog.confirm.editor.search_replace.no_more_hits.message = Search string not found. Search from the {0}? @@ -2368,6 +2436,12 @@ gui.action.execute_process.open_process.icon = folder_document.png gui.action.execute_process.open_process.mne = O gui.action.execute_process.open_process.tip = Open selected process in the Design View. +gui.action.connection.open_edit_connection.label = Open/Edit Connection +# replace with plug_edit when ready +gui.action.connection.open_edit_connection.icon = plug.png +gui.action.connection.open_edit_connection.mne = O +gui.action.connection.open_edit_connection.tip = Open selected connection + gui.label.repository_location.location_entry_name.label = Name gui.label.repository_location.location_entry_name.mne = N @@ -2401,6 +2475,9 @@ gui.dialog.error.repository_entry_with_same_name_already_exists.message = Reposi gui.dialog.error.repository_move_same_folder.title = Identical location gui.dialog.error.repository_move_same_folder.message = The source and target location are identical, cannot move entry. +gui.dialog.error.repository_copy_repository.title = Cannot copy/move repository +gui.dialog.error.repository_copy_repository.message = A repository can neither be copied nor moved to another repository. + gui.dialog.error.repository_copy_same_folder.title = Identical location gui.dialog.error.repository_copy_same_folder.message = The source and target location are identical, cannot copy entry. @@ -2411,9 +2488,9 @@ gui.dialog.error.repository_copy_into_subfolder.title = Invalid location gui.dialog.error.repository_copy_into_subfolder.message = The target location is a subfolder of the source location, cannot copy entry. gui.label.context.input.label = Process input -gui.label.context.input.icon = 16/plug_right.png +gui.label.context.input.icon = 16/gearwheel_right.png gui.label.context.output.label = Process output -gui.label.context.output.icon = 16/plug_left.png +gui.label.context.output.icon = 16/gearwheel_left.png gui.label.macros.label = Macros gui.label.macros.icon = 16/keyboard_key_a.png @@ -2439,19 +2516,19 @@ gui.action.context.apply.icon = check.png gui.action.context.input.add_row.label = Add Row gui.action.context.input.add_row.tip = Add a new process input port. It will not be shown in the process unless all other input ports are connected. -gui.action.context.input.add_row.icon = plug_add.png +gui.action.context.input.add_row.icon = plus.png gui.action.context.input.delete_row.label = Delete Row gui.action.context.input.delete_row.tip = Delete the selected procss input ports. -gui.action.context.input.delete_row.icon = plug_delete.png +gui.action.context.input.delete_row.icon = delete.png gui.action.context.output.add_row.label = Add Row gui.action.context.output.add_row.tip = Add a new process output port. It will not be shown in the process unless all other output ports are connected. -gui.action.context.output.add_row.icon = plug_add.png +gui.action.context.output.add_row.icon = plus.png gui.action.context.output.delete_row.label = Delete Row gui.action.context.output.delete_row.tip = Delete the selected process output ports. -gui.action.context.output.delete_row.icon = plug_delete.png +gui.action.context.output.delete_row.icon = delete.png gui.action.cron_help.label = gui.action.cron_help.mne = W @@ -2604,6 +2681,12 @@ gui.dialog.save_over_with_new_version.message = The process file you are writing # Miscellaneous Dialogs ######################################### +gui.dialog.error.extension_unknown.title = Extension unknown +gui.dialog.error.extension_unknown.message = The extension ''{0}'' cannot be found on the Rapidminer Marketplace. +gui.dialog.error.marketplace_connection_error.title = Connection error +gui.dialog.error.marketplace_connection_error.message = Unable to connect to the RapidMiner Marketplace. +gui.progress.search_extension_on_mp.label = Searching Marketplace + ## UpdateDialog gui.action.update_manager.label = Marketplace (Updates and Extensions)... gui.action.update_manager.icon = download.png @@ -2940,6 +3023,10 @@ gui.action.authentication.remember.label = Remember password gui.action.authentication.remember.mne = R gui.action.authentication.remember.tip = Check to remember this password permanently. +gui.action.authentication.saml.label = Use Enterprise SSO +gui.action.authentication.saml.mne = S +gui.action.authentication.saml.tip = Use Enterprise SSO. + ## UsageStatistics transmission / TransmissionDialog gui.dialog.transmit_usage_statistics.title = Transmit Operator Usage Statistics gui.dialog.transmit_usage_statistics.icon = chart_column.png @@ -3331,6 +3418,7 @@ gui.dialog.parameter.regexp.regular_expression.regexp_options.multiline_mode.tip gui.dialog.parameter.regexp.regular_expression.regexp_options.dotall_mode.tip = In dotall mode, the expression . matches any character, including a line terminator. By default this expression does not match line terminators. gui.dialog.parameter.regexp.regular_expression.regexp_options.unicode_case.tip = If checked, case-insensitive matching is done in a manner consistent with the Unicode Standard. gui.dialog.parameter.regexp.replacement.border = Replacement (for preview only) +gui.dialog.parameter.regexp.replacement_non_preview.border = Replacement (value for ''{0}'') gui.dialog.parameter.regexp.replacement.tip = The text which replaces the search results in the preview. gui.dialog.parameter.regexp.item_shortcuts.border = Item Shortcuts gui.dialog.parameter.regexp.item_shortcuts.tip = Items which should be described by the regular expression. Double click to add item to the regular expression. @@ -3345,21 +3433,24 @@ gui.dialog.parameter.regexp.inline_search.search = Text gui.dialog.parameter.regexp.inline_search.replaced = Result preview # OperatorInfoScreen & OperatorInfoPanel panel -gui.label.input_ports.label = Input -#gui.label.input_ports.icon = 16/arrow_right.png -gui.label.input_ports.icon = 16/plug_right.png -gui.label.output_ports.label = Output -#gui.label.output_ports.icon = 16/arrow_left.png -gui.label.output_ports.icon = 16/plug_left.png -gui.label.inner_sources.label = Inner source -#gui.label.inner_sources.icon = 16/arrow_left_green.png -gui.label.inner_sources.icon = 16/plug_left.png -gui.label.inner_sinks.label = Inner sink -#gui.label.inner_sinks.icon = 16/arrow_right_green.png -gui.label.inner_sinks.icon = 16/plug_right.png - -gui.label.subprocesses.label = Subprocesses -gui.label.subprocesses.icon = 16/elements_tree.png +gui.label.operator_info_screen.capabilities.label = Capabilities +gui.label.operator_info_screen.capabilities.icon = 24/briefcase2.png +gui.label.operator_info_screen.ports.label = Ports +gui.label.operator_info_screen.ports.icon = 24/plug2.png +gui.label.operator_info_screen.subprocess.label = Subprocess +gui.label.operator_info_screen.subprocess.icon = 24/elements_tree.png + +gui.label.operator_info_screen.input_ports.label = Input +gui.label.operator_info_screen.input_ports.icon = plug_right.png +gui.label.operator_info_screen.output_ports.label = Output +gui.label.operator_info_screen.output_ports.icon = plug_left.png +gui.label.operator_info_screen.inner_sources.label = Inner source +gui.label.operator_info_screen.inner_sources.icon = plug_left.png +gui.label.operator_info_screen.inner_sinks.label = Inner sink +gui.label.operator_info_screen.inner_sinks.icon = plug_right.png + +gui.label.operator_info_screen.tab.overview.label = Overview +gui.label.operator_info_screen.tab.description.label= Description #NewOperatorDialog gui.label.new_op_dialog.search_text.label = Search text: @@ -3400,7 +3491,7 @@ gui.tabs.operator_info_panel.deprecation.mne = D gui.tabs.operator_info_panel.deprecation.tip = Deprecation information. gui.tabs.operator_info_panel.ports.label = Ports -gui.tabs.operator_info_panel.ports.icon = plug.png +gui.tabs.operator_info_panel.ports.icon = plug2.png gui.tabs.operator_info_panel.ports.mne = P gui.tabs.operator_info_panel.ports.tip = The input and output ports of the operator @@ -4191,6 +4282,8 @@ gui.label.meta_data_stats.headers.type.tip = Click to sort via attribute type. gui.label.meta_data_stats.headers.missing.label = Missing gui.label.meta_data_stats.headers.missing.tip = Click to sort via the number of missing values. gui.label.meta_data_stats.headers.stats.label = Statistics +gui.progress.statistics_calculation.label = Calculating statistics +gui.label.meta_data_stats.cancelled.label = Calculation cancelled gui.label.data_view.open_in.label = Open in gui.label.data_view.filter.label = Filter ({0} / {1} examples): @@ -4331,6 +4424,7 @@ gui.progress.creating_display.label=Creating Display gui.progress.create_folder.label=Creating Folder gui.progress.refreshing.label=Refreshing gui.progress.download_from_repository.label=Downloading +gui.progress.download_connection_from_repository.label=Loading connection ({0}) gui.progress.download_md_from_repository.label=Retrieving Meta Data gui.progress.store_process.label=Storing Process gui.progress.store_ioobject.label=Storing {0} @@ -4350,6 +4444,7 @@ gui.progress.send_report_to_bugzilla.label = Sending bug report to BugZilla gui.progress.run_remote_now.label = Start process on RapidMiner Server gui.progress.db_clear_cache.label = Refresh metadata gui.progress.AbstractReader.transform_metadata.label = Transforming metadata ({0}) +gui.progress.RepositorySource.precheck_metadata.label = Checking repository location ({0}) gui.progress.import_binary.label = Loading Binary File gui.progress.log_in_to_updateserver.label = Logging in to Update Server gui.progress.log_out_frm_updateserver.label = Logging out from Update Server @@ -4364,6 +4459,8 @@ gui.progress.recover_process.label = Recovering process gui.progress.opening_license_page.label = Opening license page gui.progress.opening_documentation_page.label = Opening documentation page gui.progress.downsampling_data.label = Downsampling data +gui.progress.test_connection.label = Testing connection + ######################################## # Splash Screen @@ -4929,11 +5026,11 @@ gui.label.unknown_version = unknown #ConfigurableDialog gui.progress.loading_information.label = Loading connection information -gui.action.manage_configurables.label = Manage Connections +gui.action.manage_configurables.label = Manage Connections (Legacy) gui.action.manage_configurables.tip = Add, edit or remove all available connections. gui.action.manage_configurables.icon = gearwheel_tool.png -gui.dialog.configurable_dialog.title = Manage Connections +gui.dialog.configurable_dialog.title = Manage Connections (Legacy) gui.dialog.configurable_dialog.message = Here you can create, edit or remove all available connections. gui.dialog.configurable_dialog.icon = gearwheel_tool.png @@ -5040,6 +5137,10 @@ gui.action.configurable_creation_dialog.cancel.label = Cancel gui.action.configurable_creation_dialog.cancel.icon = delete.png gui.action.configurable_creation_dialog.cancel.tip = Close this dialog without creating a connection. +gui.action.configurable_dialog.convert_configurable.label = Convert +gui.action.configurable_dialog.convert_configurable.icon = plug_right.png +gui.action.configurable_dialog.convert_configurable.tip = Convert the configurable to a repository connection. + gui.action.configurable_dialog.test_configurable.label = Test gui.action.configurable_dialog.test_configurable.icon = gearwheel_unknown.png gui.action.configurable_dialog.test_configurable.tip = Test the connection settings. @@ -5674,8 +5775,8 @@ gui.dialog.function.process.macro.parameters = macro ( Nominal macro name ) gui.dialog.function.process.macro.group = Advanced functions gui.dialog.function.process.attribute.name = attribute() -gui.dialog.function.process.attribute.help = Attribute -gui.dialog.function.process.attribute.description = Delivers the value of the attribute with the name specified by the first argument as string. \n\nExamples: attribute(\"myAttribute\") +gui.dialog.function.process.attribute.help = Attribute Eval +gui.dialog.function.process.attribute.description = Delivers the value of the attribute with the name specified by the first argument as expression. Can take one of the type constants specifying the expected type as a second argument. If the first argument is not constant, i.e. depends on attribute values, a second argument is mandatory. \n\nExamples: attribute(\"att\" + %{macro}); attribute(\"att\" + [integerAttribute], REAL) gui.dialog.function.process.attribute.parameters = attribute ( Nominal attribute name ) gui.dialog.function.process.attribute.group = Advanced functions @@ -6106,6 +6207,7 @@ gui.component.global_search.operators.category.title = Operators gui.component.global_search.repository.category.title = Repository gui.component.global_search.action.category.title = Actions gui.component.global_search.marketplace.category.title = Marketplace +gui.component.global_search.academy.category.title = Academy gui.progress.global_search.remote_repo.search_index.label = Creating fast search index for {0} gui.progress.global_search.remote_repo.search_index_full.label = Creating full search index for {0} @@ -6116,6 +6218,7 @@ gui.progress.global_search.manager.init_index.operator.label = Indexing operator gui.progress.global_search.manager.init_index.repository.label = Indexing repositories gui.progress.global_search.manager.init_index.action.label = Indexing actions gui.progress.global_search.manager.init_index.marketplace.label = Indexing Marketplace content +gui.progress.global_search.manager.init_index.academy.label = Indexing Academy content gui.action.global_search.toolbar.label = Filter gui.action.global_search.toolbar.tip = Select the category you want to search in. @@ -6125,6 +6228,10 @@ gui.label.global_search.run_search.label = Search gui.label.global_search.run_search.tip = Search for anything (operators, data, connections, ...) gui.label.global_search.category_pending.label = Searching... +gui.label.global_search.academy.default.icon = academy_link.png +gui.label.global_search.academy.video.icon = academy_video.png +gui.label.global_search.academy.knowledgebase_article.icon = academy_page.png + gui.label.global_search.marketplace.vendor.label = Vendor gui.label.global_search.marketplace.vendor_supported.label = SUPPORTED gui.label.global_search.marketplace.vendor_supported.tip = Click to learn about our Customer Support & Maintenance Policy for extensions. @@ -6150,7 +6257,7 @@ gui.dialog.message.feedback_form.success.icon = ok.png ########################################################### #Operator Restriction Violated -########################################################## +########################################################### gui.dialog.error.blacklist_violation.connectors.message = You are trying to use an operator that has been blacklisted by the Admin. \n\n Operator Name: {0} gui.icon.plugin_blacklisted.icon = lock.png gui.label.plugin_blacklisted.tip = The usage of this extension has been prohibited by your administrator policies.
      Ask your system administrator to whitelist the extension key: {0} @@ -6161,3 +6268,210 @@ gui.notification.blacklisted_operator.icon = lock.png gui.action.close_blacklisted_operator_notification.label = Okay gui.action.close_blacklisted_operator_notification.icon = check.png gui.action.close_blacklisted_operator_notification.mne = O + + +########################################################### +# Connections +########################################################### +gui.label.connection.metadata.connection_type.message = Connection ''{0}'' of type {1} +gui.label.connection.metadata.connection_type.tags.label = Tags: +gui.label.connection.type.unknown.label = Unknown +gui.label.connection.type.unknown.icon = question.png +gui.action.connection.clear_cache_now.label = Clear Cache now +gui.action.connection.clear_cache_now.tip = Click to clear the cache right now and restart RapidMiner +gui.progress.connection.clear_cache.label = Clear Connections Cache +gui.dialog.confirm.connection.clear_cache_now.title = Clear Cache and Restart? +gui.dialog.confirm.connection.clear_cache_now.message = This will clear your local connection files cache and restart Rapidminer Studio. Proceed? + +gui.action.save_connection.label = Save +gui.action.save_connection.mne = S +gui.action.save_connection.icon = check.png +gui.action.save_connection.tip = Save the connection. + +gui.action.connection.save_injection.label = Save +gui.action.connection.save_injection.mne = S +gui.action.connection.save_injection.icon = check.png +gui.action.connection.save_injection.tip = Save the injection configuration. + +gui.action.cancel_connection_edit.label = Cancel +gui.action.cancel_connection_edit.mne = C +gui.action.cancel_connection_edit.icon = delete.png +gui.action.cancel_connection_edit.tip = Abort the connection configuration without saving changes + +gui.action.close_connection_edit.label = Close +gui.action.close_connection_edit.mne = C +gui.action.close_connection_edit.icon = delete.png +gui.action.close_connection_edit.tip = Close this connection dialog + +gui.action.connection.cancel_injection_edit.label = Cancel +gui.action.connection.cancel_injection_edit.mne = C +gui.action.connection.cancel_injection_edit.icon = delete.png +gui.action.connection.cancel_injection_edit.tip = Abort the injection configuration without saving changes + +gui.action.connection.create_new.create.label = Create +gui.action.connection.create_new.create.mne = A +gui.action.connection.create_new.create.icon = check.png +gui.action.connection.create_new.create.tip = Create the connection now and then configure it. + +gui.action.connection.create_new.cancel.label = Cancel +gui.action.connection.create_new.cancel.mne = C +gui.action.connection.create_new.cancel.icon = delete.png +gui.action.connection.create_new.cancel.tip = Cancel the connection creation and close this dialog. + +gui.action.test_connection.label = Test connection +gui.action.test_connection.mne = T +gui.action.test_connection.icon = test_connection.png +gui.action.test_connection.tip = Test the connection. + +gui.action.edit_connection.label = Edit +gui.action.edit_connection.mne = E +gui.action.edit_connection.icon = pencil.png +gui.action.edit_connection.tip = Edit the connection. + +gui.action.inject_connection_parameter.label = Set injected parameters +gui.action.inject_connection_parameter.mne = I +gui.action.inject_connection_parameter.icon = injection2.png +gui.action.inject_connection_parameter.tip = An injected parameter is a parameter whose value is provided by an external source. + +gui.progress.saving_connection.label = Saving connection ({0}) + +gui.dialog.confirm.cancel_connection_edit.title = Unsaved Changes +gui.dialog.confirm.cancel_connection_edit.message = Discard all unsaved changes? +gui.dialog.confirm.cancel_connection_edit.icon = door_exit.png + +gui.dialog.error.saving_connection_failed.title = Connection could not be saved +gui.dialog.error.saving_connection_failed.message = Connection {0} could not be stored at {1}.

      Reason: {2} + +gui.label.connection.view_connection.label = View connection - {0} +gui.label.connection.edit_connection.label = Edit connection - {0} +gui.label.connection.location.label = Location +gui.label.connection.location_description.label = Connections are now repository items. +gui.label.connection.description.label = Description +gui.label.connection.description.tip = Enter some description for this connection, so that you can later search and filter your connections. +gui.label.connection.description_description.label = You can enter a short description for this connection here. +gui.label.connection.tags.label = Tags (Optional) +gui.label.connection.tags.tip = Add tags so that you can easier filter and find your connections. +gui.label.connection.tags_description.label = Add tags so that you can easier filter and find your connections. +gui.label.connection.no_tags.label = No Tags +gui.label.connection.add_tag_hint.label = type and press enter to create tag +gui.label.connection.info_panel.label = Info +gui.label.connection.sources_panel.label = Sources +gui.label.connection.injected_parameter.label = injected parameter +gui.label.connection.injected_parameter.icon = 16/injection2.png +gui.label.connection.unknown_vp_no_help.label = This source is not known. Try updating RapidMiner Studio. +gui.label.connection.unknown_vp.label = Unknown source ({0}) +gui.label.connection.unknown_vp.icon = sign_warning.png +gui.action.connection.install_extension_unknown_vp.label = Search for missing ''{0}'' extension on the Marketplace +gui.label.connection.unknown_type_no_help.label = Unknown connection ({0}) +gui.label.connection.unknown_type.label = Unknown connection +gui.action.connection.install_extension_unknown_type.label = (Search for missing ''{0}'' extension on the Marketplace) + +gui.dialog.error.connection_read_error.title = Could not load connection +gui.dialog.error.connection_read_error.message = {0} + +gui.label.connection.group.label = Group +gui.label.connection.parameter.label = Parameter +gui.label.connection.error_message.label = Error Message +gui.label.connection.test.cancel_testing.label = Cancel Testing + + +gui.label.connection.test.running.icon = loading.gif +gui.label.connection.test.cancel.icon = delete.png +gui.label.connection.test.not_supported.icon = hint.png +gui.label.connection.test.warning.icon = sign_warning2.png +gui.label.connection.test.success.icon = check.png + +gui.label.connection.validation.warning.icon = sign_warning2.png +gui.label.connection.validation.success = Validation successful. +gui.label.connection.validation.failed = Some values are not set correctly for this connection. +gui.label.connection.validation.value_missing = {0}: Value not set +gui.label.connection.validation.value_missing_placeholder = {0}: Value not set; check for missing/erroneous placeholders +gui.label.connection.validation.value_not_injectable = {0}: Value not injectable; check value providers +gui.label.connection.validation.rapidminer_vault_local = Source RapidMiner Server cannot be used for this local connection. + +gui.action.connection.test.copy_result.label = Copy text +gui.action.connection.test.copy_result.icon = copy.png +gui.action.connection.test.copy_result.tip = Copy the test result to your clipboard. +gui.label.connection.test.success = Test successful. +gui.label.connection.test.object_null = The test object was null. +gui.label.connection.test.running = Running test... +gui.label.connection.test.not_registered = Connection type unknown. Cannot test. +gui.label.connection.test.not_implemented = Test functionality not implemented. +gui.label.connection.test.unexpected_error = Unexpected Error: {0} + +gui.label.connection.test.connection_failed = {0} +gui.label.connection.test.injection_failed = Injection failed. Check the highlighted parameters. + +gui.label.connection.parameter.remote_repository.rapidminer_vault.valueprovider.no_configuration.tip = Please configure this in the RapidMiner Server web interface.

      This can only be used for connections stored on the same Server. +gui.label.connection.valueprovider.type.chaining.label = Chaining +gui.label.connection.valueprovider.type.macro_value_provider.label = Macros +gui.label.connection.valueprovider.type.macro_value_provider.injector_selection.label = {0} (macro key: {1}) +gui.label.connection.valueprovider.type.macro_value_provider.injected_parameter.label = injected by {0} (macro key: {1}) + +gui.label.connection.parameter.macro_value_provider.valueprovider.prefix.label = Prefix (Optional) +gui.label.connection.parameter.macro_value_provider.valueprovider.prefix.tip = If left empty, the looked up macro equals the injection key (e.g. "mykey"). Otherwise, the looked up macro will be prefixed (e.g. "prefix_mykey"). + +gui.dialog.inject_connection_parameter.title = Set injected parameters +gui.dialog.inject_connection_parameter.icon = injection2.png +gui.dialog.inject_connection_parameter.message = Select parameters to inject and the source from where they are injected from.
      You can configure sources in the Sources tab. +gui.field.inject_parameters.prompt = Search parameters + +gui.dialog.connection.valueprovider.needs_configuration.icon = sign_warning.png +gui.dialog.connection.valueprovider.needs_configuration.tip = Needs configuration +gui.dialog.connection.valueprovider.header_information = This is a list of all available sources that parameters can be injected from.
      In order to use them, you need to ensure they are configured correctly.
      Some sources don't need any configuration. +gui.dialog.connection.valueprovider.header_information_viewmode = This is the list of all sources currently used in this connection.
      In order to use them, you need to ensure they are configured correctly. +gui.dialog.connection.valueprovider.header_information_viewmode_empty = This connection uses no sources. +gui.dialog.connection.valueprovider.needs_no_configuration.label = No additional configuration. +gui.label.connection.valueprovider.key_for_injection.label = Key for injection: {0} +gui.dialog.inject_connection.value_provider_configuration_warning =

      One or more of the sources you have selected are not configured.
      You can configure them in the Sources tab.

      +gui.dialog.inject_connection.select = Select... +gui.action.inject_connection_clear_filter.icon = x-mark.png +gui.action.inject_connection_clear_filter.tip = Clear this search +gui.action.inject_connection_clear_filter.highlight_icon = x-mark_orange.png +gui.dialog.inject_connection_parameter.no_parameters.label = There are no injectable parameters in this group. +gui.dialog.inject_connection_parameter.no_parameters.icon = information.png +gui.dialog.inject_connection_parameter.no_results.label = There are no results for your search term. +gui.dialog.inject_connection_parameter.no_results.icon = information.png + +gui.dialog.connection.create_new_connection.title = Create a new connection +gui.label.connection.create_new.info.label = Connections are now objects that are stored in the repository. You can enter more details to your connection in the next steps. +gui.label.connection.create_new.name.label = Connection Name: +gui.label.connection.create_new.name.prompt = Start typing a name +gui.label.connection.create_new.name.tip = Enter the name of your new connection. +gui.label.connection.create_new.type.label = Connection Type: +gui.label.connection.create_new.type.tip = Select the type of your new connection. +gui.label.connection.create_new.repository.label = Repository: +gui.label.connection.create_new.repository.tip = Select the repository where your new connection should be created in. + +gui.label.connection.create_new.error.name_empty = Please enter a name for the connection. +gui.label.connection.create_new.error.name_duplicate = Name already taken. +gui.label.connection.create_new.error.type_empty = Please select the connection type. +gui.label.connection.create_new.error.repository_empty = Please select a repository. +gui.label.connection.create_new.error.repo_no_conn_folder = Repository has no Connections folder. +gui.label.connection.create_new.error.repo_conn_folder_retrieval = Cannot find Connections folder. +gui.label.connection.create_new.error.conversion = Conversion of connection failed: {0} +gui.label.connection.create_new.status.working = Creating connection, this may take a few seconds... +gui.label.connection.create_new.status.failed = Failed to create connection: {0} +gui.label.connection.warning.icon = sign_warning.png +gui.label.connection.information.icon = information.png +gui.label.connection.working.icon = loading.gif +gui.label.connection.unknown_provider_warning.label = There is no handler for this connection. + +gui.action.create_connection.label = Create Connection +gui.action.create_connection.icon = plug_add.png +gui.action.create_connection.tip = Create a new connection. This is the new, recommended way to connect to external data sources since RapidMiner Studio 9.3. + +gui.cards.result_view.connection.icon = plug.png + +gui.label.deprecation.warning.manage_configurables.label = These connections use the old mechanism, which will be deprecated in a future release of Studio. To convert an existing connection, select it and press the Convert button. To create a new connection with the new mechanism, go to Connections \u2192 Create Connection, or right click on any repository. + +gui.label.connection.valueprovider.hint.injected_parameter_template.label = Injected by {0} +gui.label.connection.save_unknown_vp = Unknown source ''{0}''. Update RapidMiner or try searching the marketplace to install this source. + +gui.label.connection.operator_parameter.connection_entry.any.desc = Select a connection of any type from a repository +gui.label.connection.operator_parameter.connection_entry.type.desc = Select a connection of type ''{0}'' from a repository + +gui.label.connection.placeholder_encrypted.label = ******** + +gui.label.connection.tooltip.full_key.label = Full key +gui.label.connection.tooltip.description.label = Description diff --git a/src/main/resources/com/rapidminer/resources/i18n/LogMessages.properties b/src/main/resources/com/rapidminer/resources/i18n/LogMessages.properties index 14f7ee2a6..4a995b83a 100644 --- a/src/main/resources/com/rapidminer/resources/i18n/LogMessages.properties +++ b/src/main/resources/com/rapidminer/resources/i18n/LogMessages.properties @@ -9,6 +9,7 @@ com.rapidminer.gui.flow.ProcessRenderer.loading_operator_group_colors_error=Cann com.rapidminer.gui.flow.ProcessRenderer.loading_io_object_colors_error=Cannot load io object colors for plugin {0} com.rapidminer.gui.tools.SwingTools.loading_operator_group_colors_error=Cannot load operator group colors. com.rapidminer.gui.tools.SwingTools.retina_detection_error=Error during Retina display detection. +com.rapidminer.gui.tools.SwingTools.failed_hashmap_fix=Failed to fix concurrent access problem of the prompt support. Please notify the developers when you see this. com.rapidminer.gui.MainFrame.font_load_failed=Failed to load font. Reason: {0} com.rapidminer.gui.tools.ResourceAction.add_action_key_error=Cannot add action {0} to input map: no accelerator defined. com.rapidminer.gui.tools.ResourceAction.key_not_found_converting_upper_case=Mnemonic key {0} not found for action {1} ({2}), converting to upper case. @@ -215,6 +216,7 @@ com.rapidminer.repository.RepositoryTree.accepting_flavor_error=Cannot accept dr com.rapidminer.repository.RepositoryTree.error_during_drop=Error during drop: {0} com.rapidminer.repository.RepositoryTree.error_during_copying=Error during copy operation. com.rapidminer.repository.RepositoryTree.error_during_deletion=Error during delete operation. +com.rapidminer.repository.RepositoryTree.error_resolving_connections_folder=Error trying to resolve Connections folder for target repository. com.rapidminer.repository.RepositoryTree.parameter_missing.target_location=A copy/move error occurred: The target location is unknown. com.rapidminer.repository.RepositoryTree.parameter_missing.target_path=A copy/move error occurred: The target path is unknown. com.rapidminer.repository.RepositoryTree.parameter_missing.source_path=A copy/move error occurred: The source path is unknown. @@ -254,6 +256,7 @@ com.rapidminer.tools.jdbc.DatabaseService.reading_jdbc_driver_description_file_e com.rapidminer.tools.jdbc.DatabaseService.reading_jdbc_driver_description_file_outermost_tag_error=JDBC driver description file {0}: outermost tag must be ! com.rapidminer.tools.jdbc.DatabaseService.registering_jdbc_driver_description_error=JDBC driver description: cannot register {0}: {1} com.rapidminer.tools.jdbc.DatabaseService.merging_jdbc_driver_information=Merging JDBC driver information for {0}. +com.rapidminer.tools.jdbc.DatabaseService.error_loading_resources_jdbc_properties=Cannot load JDBC properties from program resources. com.rapidminer.tools.jdbc.DatabaseService.error_loading_commercial_jdbc_properties=Error loading commercial JDBC drivers. com.rapidminer.tools.jdbc.DatabaseService.skipping_commercial_db_drivers=Skipped installation of commercial JDBC driver with URL {0} as commercial JDBC drivers are not permitted by the current license. com.rapidminer.gui.renderer.RendererService.initializing_io_object_description_from_plugin_error=Cannot initialize io object description of plugin {0}: Cannot parse document: {1} @@ -354,6 +357,8 @@ com.rapidminer.repository.RepositoryManager.education.insufficient_information = com.rapidminer.repository.RepositoryManager.education.not_reachable = Community repository ''{0}'' could not be reached. Check your internet connection. com.rapidminer.repository.RepositoryManager.education.no_connection = Community repository ''{0}'' could not be connected. Check your internet connection. com.rapidminer.repository.RepositoryManager.education.success = Community repository ''{0}'' added. +com.rapidminer.repository.RepositoryManager.filter_failure = Repository Filter failure +com.rapidminer.repository.RepositoryManager.repository_does_not_exist = No such repository com.rapidminer.gui.tools.ResourceDockKey.missing_icon=Missing icon: {0} com.rapid_i.deployment.update.client.UpdateDialog.ignoring_update_check=This is a development build. Ignoring update check. com.rapidminer.gui.MetaDataUpdateQueue.error_while_updating=While updating process editors: {0} @@ -563,7 +568,7 @@ com.rapidminer.gui.plotter.PlotterControlPanel.instatiating_plotter_error=Plotte com.rapidminer.gui.new_plotter.engine.jfreechart.ChartRendererFactory.null_value_source=Value Source was null! com.rapidminer.gui.new_plotter.data.DimensionConfigData.null_dimension_config=Dimension Config was null! com.rapidminer.gui.meta_data_view.calc_interrupted=Meta data calculation was interrupted! -com.rapidminer.gui.meta_data_view.calc_sync_broken=Meta data calculation was aborted! +com.rapidminer.gui.meta_data_view.calc_cancelled=Meta data calculation was aborted! com.rapidminer.gui.meta_data_view.calc_error=Failed to calculate meta data! com.rapidminer.gui.processeditor.XMLEditor.failed_to_parse_process=Invalid process xml! @@ -799,6 +804,8 @@ com.rapidminer.repository.global_search.RepositorySearchManager.error.initial_in com.rapidminer.repository.global_search.RepositorySearchManager.error.initial_index_error_rest_folder = Failed to index REST repository folder {0} for Global Search: {1}! com.rapidminer.repository.global_search.RepositorySearchManager.error.initial_index_fallback_rest_folder = Falling back to normal indexing com.rapidminer.repository.global_search.RepositorySearchManager.error.initial_index_error_md_reading = Failed to read meta data of repository entry for Global Search! +com.rapidminer.repository.global_search.RepositorySearchManager.error.initial_index_error_md_connection_reading = Failed to read meta data of repository connection entry {0} for Global Search! + com.rapidminer.gui.processeditor.global_search.OperatorSearchManager.error.no_key = Cannot insert operator, key was null! com.rapidminer.gui.processeditor.global_search.OperatorSearchManager.error.operator_browse_error = Cannot preview operator, creation failed: {0}! @@ -812,6 +819,8 @@ com.rapidminer.gui.actions.search.ActionsGlobalSearchManager.error.delete_views_ com.rapidminer.gui.actions.search.ActionsGlobalSearchManager.error.find_action_error = Cannot figure out if action is registered to Global Search! com.rapidminer.gui.actions.search.ActionsGlobalSearchManager.error.delete_dockables_error = Failed to remove panel actions from Global Search! +com.rapidminer.repository.global_search.ConnectionSearchManager.conversion_failed = Global Search conversion from Repository to Connection search entry failed! + com.rapidminer.gui.globalsearch.MarketplaceGlobalSearchGUIProvider.error.lazy_loading = Failed to load extension icon. Using fallback. com.rapidminer.gui.globalsearch.MarketplaceGlobalSearchGUIProvider.error.lazy_loading_code = Failed to load {0} extension icon. Status code was {1}. Using fallback. @@ -888,12 +897,51 @@ com.rapidminer.repository.PersistentContentMapperStore.entry_rename_update_faile com.rapidminer.gui.import.could_not_create_dateformat = Could not create SimpleDateFormat instance com.rapidminer.gui.import.could_not_read_from_dataset = Could not read data from dataset -com.rapidminer.tools.FontTools.system_font_loading.failed = Could not load system fonts. - com.rapidminer.tools.ListenerTools.uncaught_exception = Undefined Exception thrown by Listener. + +com.rapidminer.ProcessLogOperator.unspecified_logfile = No file was specified for log operator ''{0}''. Will not write log to file. +com.rapidminer.ProcessLogOperator.empty_loglist = No log values were specified for log operator ''{0}''. Will not write log. + +com.rapidminer.connection.encryption.could_not_retrieve_key = Could not retrieve encryption key +com.rapidminer.connection.encryption.could_not_encrypt = Could not encrypt value +com.rapidminer.connection.encryption.could_not_decrypt = Could not decrypt value + +com.rapidminer.operator.ports.metadata.ConnectionInformationMetaData.json_processing_error = Failed to write the Connection Information Configuration com.rapidminer.tools.net.UrlFollower.already_connected_properties = Already connected, unable to keep request properties during redirect! com.rapidminer.tools.net.UrlFollower.already_connected_body = Already connected, unable to set request body! com.rapidminer.MacroHandler.invalid_temp_dir = Path stored in system property "java.io.tmpdir" could not be accessed: ''{0}'' using ''{1}'' instead. - -com.rapidminer.gui.viewer.metadata.actions.OpenChartAction.cannot_show_visualization = Could not open visualizations. \ No newline at end of file +com.rapidminer.tools.FontTools.system_font_loading.failed = Could not load system fonts. +com.rapidminer.gui.tools.dnd.ExtendedJListTransferHandler.unexpected_error = Unexpected error during drag and drop. + +com.rapidminer.extension.html5charts.gui.renderer.AbstractVisualizationRenderer.no_adapter = No adapter for Visualizations chart data found for class ''{0}''. +com.rapidminer.extension.html5charts.gui.renderer.AbstractVisualizationRenderer.no_initial_config = Visualization for class ''{0}'' cannot be configured but initial configuration was not set! + +com.rapidminer.gui.viewer.metadata.actions.OpenChartAction.cannot_show_visualization = Could not open visualizations. + +com.rapidminer.connection.valueprovider.handler.MacroValueProviderHandler.retrieval_failed = Retrieval of macro ''{0}'' in value provider ''{1}'' failed. +com.rapidminer.connection.valueprovider.handler.MacroValueProviderHandler.no_context = No operator context given, cannot resolve macros for value provider ''{0}''. +com.rapidminer.connection.valueprovider.handler.MacroValueProviderHandler.macro_not_found = Macro Source did not find macro named ''{0}''. + +com.rapidminer.connection.gui.model.ConnectionModelConverter.removed_nonexisting_other_file = Removed non-existing paths +com.rapidminer.connection.gui.model.ConnectionModelConverter.cloning_connection_failed = Cloning of connection failed, creating new connection. +com.rapidminer.connection.gui.model.ConnectionModelConverter.cloning_connection_configuration_failed = Cloning of connection configuration failed, creating new connection configuration. +com.rapidminer.connection.gui.model.TextChangedDocumentListener.document_get_text_failed = Could not read text from document. +com.rapidminer.connection.gui.actions.TestConnectionAction.testing_failed = Connection test failed unexpectedly. +com.rapidminer.connection.gui.ConnectionEditDialog.fallback_gui_used = The connection type {0} failed to provide a custom UI! Using basic fallback UI. +com.rapidminer.connection.file_cache.unable_to_delete = Could not delete file cache for connections. Total errors: {1}\n{0} + +com.rapidminer.connection.injection.value_not_injected = Injection for parameter ''{0}'' did not work, Source ''{1}'' did not inject anything. +com.rapidminer.connection.injection.missing_value_provider = Injection might not be complete: Missing value provider handler for type ''{0}'' +com.rapidminer.connection.injection.bad_vp_null = Injection failed. Source ''{0}'' broke contract and returned null instead of the injected values. + +com.rapidminer.connection.gui.ConnectionCreationDialog.creation_failed = Failed to create connection. +com.rapidminer.connection.gui.ConnectionCreationDialog.repo_conn_folder_retrieval = Failed to retrieve Connections folder. + +com.rapidminer.connection.adapter.creation_error = Cannot create new adapter with name ''{0}'': {1} +com.rapidminer.connection.adapter.no_handler_registered = No adapter handler registered for type ''{0}''. +com.rapidminer.connection.adapter.handler_already_registered = A handler for the type ''{0}'' was already registered +com.rapidminer.connection.adapter.handler_already_registered.mismatch = A handler for the type ''{0}'' was already registered and the classes don't match.\nRegistered: {1}\nMismatched: {2} +com.rapidminer.connection.adapter.connection_failed = Connection for {0} of type ''{1}'' failed: {2} +com.rapidminer.connection.adapter.conversion_failed = Conversion for {0} of type ''{1}'' failed: {2} +com.rapidminer.connection.gui.model.ConnectionModelConverter.removed_nonexisting_paths = Removed non-existing files from connection. \ No newline at end of file diff --git a/src/main/resources/com/rapidminer/resources/i18n/Settings.properties b/src/main/resources/com/rapidminer/resources/i18n/Settings.properties index b2da24cb6..b316743c3 100644 --- a/src/main/resources/com/rapidminer/resources/i18n/Settings.properties +++ b/src/main/resources/com/rapidminer/resources/i18n/Settings.properties @@ -66,6 +66,7 @@ rapidminer.preferences.subgroup.search.repository.title = Repository Search Sett rapidminer.preferences.subgroup.system.data.title = Data Management rapidminer.preferences.subgroup.system.network.title = Network +rapidminer.preferences.subgroup.system.file_cache.title = Local File Cache ############################## ## Properties General @@ -294,6 +295,9 @@ rapidminer.system.network.follow_https_to_http.description = Uncheck this option rapidminer.system.network.follow_http_to_https.title = Follow HTTP to HTTPS redirects rapidminer.system.network.follow_http_to_https.description = Uncheck this option to prohibit cross-protocol redirects from HTTP to HTTPS. +rapidminer.system.file_cache.connection.keep.title = Keep Connection Files +rapidminer.system.file_cache.connection.keep.description = Indicate how long files that belong to connections should be held in the file cache. The option never means to clear the cache at each start up. + ############################## ## Properties Proxy ############################## diff --git a/src/main/resources/com/rapidminer/resources/i18n/UserErrorMessages.properties b/src/main/resources/com/rapidminer/resources/i18n/UserErrorMessages.properties index 1ddbc5a0a..5e41a63f2 100644 --- a/src/main/resources/com/rapidminer/resources/i18n/UserErrorMessages.properties +++ b/src/main/resources/com/rapidminer/resources/i18n/UserErrorMessages.properties @@ -957,9 +957,13 @@ error.sending_mail_to_address_error.short = Cannot send mail to ''{0}''. {1} error.sending_mail_to_address_error.long = An error occurred while trying to send a mail. See log for more details. error.excel_sheet_name_too_long.name = Sheet name too long -error.excel_sheet_name_too_long.short = Sheet name ""{0}"" is too long. Only 31 characters allowed but {1} used. +error.excel_sheet_name_too_long.short = Sheet name ''{0}'' is too long. Only 31 characters allowed but {1} used. error.excel_sheet_name_too_long.long = Sheet names in Excel must not exceed 31 characters. +error.excel_sheet_name_duplicate.name = Duplicate sheet name +error.excel_sheet_name_duplicate.short = Duplicate excel sheet name ''{0}''. +error.excel_sheet_name_duplicate.long = Duplicate excel sheet name ''{0}''. Sheet names must be unique. + error.cannot_parse_expression.name = Invalid expression error.cannot_parse_expression.short = The expression "{0}" cannot be parsed. Error was: {1} error.cannot_parse_expression.long = The expression is not valid. Please have a look at the operator help for syntax information. @@ -1184,6 +1188,10 @@ error.no_such_attribute_role.name = Attribute role not present error.no_such_attribute_role.short = The given example set does not contain an attribute role {0}. error.no_such_attribute_role.long = Please check whether it's missing in the input example set erroneously or correct the settings in the parameters. +error.invalid_positive_class.name = Invalid positive class +error.invalid_positive_class.short = The specified positive class ''{0}'' does not match any of the label's values. +error.invalid_positive_class.long = Please check whether it's missing in the input example set erroneously or correct the settings in the parameters. + error.performance_criterion_class_mismatch.name = Mismatched criterion class error.performance_criterion_class_mismatch.short = The criteria classes
      ''{0}''
      and
      ''{1}''
      are not comparable. error.performance_criterion_class_mismatch.long = Different performance criteria can not be easily compared. They may vastly differ in their value range, especially in regards to the positive/negative value spectrum. If you want to compare different performance criteria or combine them for one model, you can either select to use more than one criteria in a Performance Operator or combine them with the Combine Performance Operator. @@ -1232,6 +1240,34 @@ error.aggregation.percentile.numberformat.name = Percentile number not parsable error.aggregation.percentile.numberformat.short = Percentile value could not be read. error.aggregation.percentile.numberformat.long = Expecting an integer or a floating point value but found ''{0}''. -error.jdbc.DatabaseDataReader.unknown_table.name = Database error: unknown table -error.jdbc.DatabaseDataReader.unknown_table.short = The table ''{0}'' is unknown and might not exist in the database -error.jdbc.DatabaseDataReader.unknown_table.long = \ No newline at end of file +error.connection.no_connection.name = No connection selected +error.connection.no_connection.short = No repository connection entry was selected +error.connection.no_connection.long = Choose a connection entry from a repository or connect the corresponding input port + +error.connection.wrong_entry_type.name = Wrong repository entry +error.connection.wrong_entry_type.short = The selected repository entry is not a connection +error.connection.wrong_entry_type.long = Choose a connection entry from a repository or connect the corresponding input port + +error.connection.wrong_entry_data.name = Wrong repository entry data +error.connection.wrong_entry_data.short = The selected repository entry does not contain a connection +error.connection.wrong_entry_data.long = Choose a connection entry from a repository or connect the corresponding input port + +error.connection.repository_error.name = Repository error +error.connection.repository_error.short = An error occurred while accessing repository entry ''{0}'' +error.connection.repository_error.long = + +error.connection.no_container.name = Error while resolving connection +error.connection.no_container.short = The selected/provided connection cannot be resolved +error.connection.no_container.long = + +error.connection.mismatched_type.name = Mismatched connection type +error.connection.mismatched_type.short = Expected connection of type ''{0}'' but found ''{1}'' instead +error.connection.mismatched_type.long = Choose a connection of type ''{0}'' and try again + +error.connection.adapter.no_handler_registered.name = No handler registered +error.connection.adapter.no_handler_registered.short = No handler could be found for the type ''{0]'' +error.connection.adapter.no_handler_registered.long = Please check if you need to install an extension to use this connection type + +error.connection.adapter.validation_error.name = Connection not correctly configured +error.connection.adapter.validation_error.short = Some values are not set correctly for the connection {0} of type {1}. +error.connection.adapter.validation_error.long = Please check your connection again for missing values, missing/undefined placeholders or misconfigured sources. \ No newline at end of file diff --git a/src/main/resources/com/rapidminer/resources/icons/16/@2x/academy_link.png b/src/main/resources/com/rapidminer/resources/icons/16/@2x/academy_link.png new file mode 100644 index 0000000000000000000000000000000000000000..05c5906f036c954ba7a7ccb3835d44ecfca3c2fb GIT binary patch literal 1133 zcmV-z1d{uSP)Px(CrLy>R9Fe^R&QuqRTMwxzW4Hy%%X9&*sk5gIq}aN2o5(z<~~dioeWvGj`=~T zq)e186UK%^V~L_0c2Go3Vr7UDb|vtFUfoNc$fry4;^;RYB@U@M5g@z; ztsvnRaJa!tBOZ|(!6aM<4`L4#6&_W$|45S5I3N0TXy~fM4!6ArhsL+UW%~yBwEs2K zl{6xFYRMN0%SlSD-U+(qs;CDc)^C>q0zo%4-8P^k=SHBsSvhVF?|HFcS+Po9<(xRy z(s3$TbIyJKVBd-N`(IE1>Vxwf_bnKK-_?3f^ZkJ(+f9E^Vdf+&mj%_XM7EuOmNBjY z)mTx@Q0uNe2Onk+@A2BJ1&?f9SQj_fY4D@RWQB+0Hy(42F=+(xPB6vAFu zZvy}}d4U(gvu%5omwb%@ossB`{8n%E0xyI|(lH*}wZBGhjR871tqVD*o-7vfcUSM% z7$C_pt3lUljUbz?Opk*=A-S{J z;Aw$4iJErKCyZlXB>O&N+$?b#z6~<)3rFE=d=)m&Zp?QQj>nD8NF>mxJRN~&sF%&( z2V-TK*$8OK;U`JrK`s}odDGy6QQ*x%p15?S!7Zb*C z1c=w|o@EEhm_|nT3XtF@1$M}e=>w_tpAV)&S;&kYY!B=+s?A5arV@+ta9Qy z_t2_k>%QBFv%g=;s4J0cfC3ru;rk`R0siRt6(GL?;#Yu!9HLk;+^$k6M2SRiBJ~{e zJ2?j|S3IT(0dy)9GJ7v?Ysr*Yi~V8*8jrxgGu3-Z6~qk*00000NkvXXu0mjfC@m(B literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/16/@2x/academy_page.png b/src/main/resources/com/rapidminer/resources/icons/16/@2x/academy_page.png new file mode 100644 index 0000000000000000000000000000000000000000..9242eaee709ffa1bab760be7bab2801e6ab867c7 GIT binary patch literal 821 zcmV-51Iqk~P)Px%?@2^KR9Fe^R$WLGQ4~IBe)h+o%1Beu{tGH9upXjb>cJq&C`dAV39^-X(M1p; zi6yKDB?Sq>HKT`q2nr&immYeL1fiFp!icCa12L`L_0GMWJKM-GGrNwv`qUn{bM8I& zJLi0J=Kc)8|1Owo0qUD0odEn?+F!Z*@_xKm_Jsh1n@|NDHh@75{xv}wsR@{Z5;%yh zFemXSA6)~(Lh4-P!}$23-1@3-L+`|C_`z1eH)(OtlY#B7w43P~Y zC_b|Q3WBDq8q*=|XI8+PwCjdQ`{h(3(POvS`+{oC4a08A*bjt?`d{1`?2rJ_4`WpK zlNg0ha-5NVC|rCd7%GyOjFR|03$iUFj3qA-#w6gxDo%qOH??;kqBq>-HD|Yl!r2L0 zSk8>liSyAmYi!q^1i0W$c2LGXeO$jXXQU!tSZ@LV^ct3Uj8lujnUX@+N+zq z!1>q&H}qo{yIgTy1T-+J@=27<7gNcd&UqIBE{vcPv{YjRUh0hHBCvA%`nLpn=LF_E zR#hz--#ju#ajNNvK4Qgl5s0_8&Z$A=I3h|SOCpM(aPY=6-Yuke1%&gmOg`A z1Z2iXeLWwcOy7r!A`g(9t0CVyP*nU7DoWQpix0k{H-7^Y6j6gojHQofUmN*sTWmKI z>@XtVPnTT{U3nE0rkrlDvZ=`lP*CCYU_|(jf;fe$IvP;)2TzClpAc?d=osDtGw`0O z@Eor}!~8_9oygFT-WZLB{rZamXc1S5ekXjle9Q{yBVYCyhImc;KVVJx(R2`w7?Ii* zKUj$oH{c%3LNolNOgkVb{|U@BAege&Px6f`H0a*D0xV5FwSJSEID{Hg%b%8S^wUC; z@isJpj>q^kgPzf8(<-&E1!RvQ==)0&0ZOjZN+2-_oZ41Wcc5}n)i=X_{>ADB>Px&k4Z#9R9Fe^R!e9UK@hF(nb}_we=#wuK}8Y#peF^vAm*STnh$75BnqOq@q=ja z7f@sm;vvBV7a_P&@t_H!0YBiygBZbsCdL5O^XUEgfB4jM=oo;o*OBYny)!T0ifEFWM})7rIDf;Y|oQMp5JOg`6=d zQwCWwLS%w)jIWshNwPw2oDQ<7eZs*afT2Ds!wg({-yUt?csn+8^Ht zoe%)t4*gj7TG9+3gr1hZ$6HYE_V@%QZHq;If#?%8O;Zme#vowFQoBLu4ad)I!WFJ| znzPzG-pm9|EYoyP_5S8#rfwY_2{6tYte~_qzTC;SgdPt#VVwy8Y<2?YV$(EjnUjAC zfx7149Q;<7IDvDqai%NV4z;AjO(7tI?$0GrCSMX)SJ~%N2r#a@?Vx<=1Xz(hRtkZc zD;B*)(10D7t1d~h8eV$6ALGo&BYKM&&md6uf_tm`BB$!!@xpLZQ$ljn?Sv3PBrlE1 zQq1jjZ7p;EJP#_Az(?rh!Q>fa5Ma9M$6KUT{TdxWA`N%iE)2nJWi6pp93WX&L#*26 zE9imJqPY*lp{Ka=SMV7l%J7v0lgG2|B;*V53+SnN76~v(I@(*|?nHR!u4BP98rLcb zVe9d-D@U@WXfMGVY&MQS31#@ukZ_a|GDalnwTmj<_q*DA#r)qU!+rEzDS5sZ?LC=yulgKFPn~H3|zgckb$Utx1%F!wWpN{Y%=B&u+GF7N7;_& zu_Xzn51B^mh=<)Lp!DQ}3U9a7o{h{e!7ca&jWCLJUI}jTPmuBz*aPFG=jS@RUDFqN zR`Wug@P4rSkJAa18@l*u`Gh|$*o;^3@rOQhG%{foH)6S7R=wiIkJggNxWJQDIK0Qy z<>12P3ho4x!1q{u#^7jX0!1$QTk#B~KI`O|jGTZ2cCL<_&lgc!@WIB?BSDm{ zbNP@n?twxVeLt(fwR~jvyhBrwTr9w`0y3ql&m)}~+fz2~v&+Yd?J-ga_!Rjmg6c8( z7dv<=bc4VT&W03O8sUr|{JGEnU@8(#$DFP}x(fUTB0v}{>t|zu00000NkvXXu0mjf D=~mRV literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/16/@2x/gearwheel_left.png b/src/main/resources/com/rapidminer/resources/icons/16/@2x/gearwheel_left.png new file mode 100644 index 0000000000000000000000000000000000000000..546b7716efe6f443616b4d9a1d5863b3b3201778 GIT binary patch literal 1776 zcmVPx*s!2paR9FeER()(!RTMw>y|(Mxwd=O7u#L%pjWIC87#9*Fx)8*uAtqqd82{NI zDw33iU=}vP5`}FpK_-Mm7#~56W*Gd_s5m2kh#&)j5ZM;k_qy(juIu;v^4zz+*Z0h+`&DFV|vei6)q1z>x8qBe*m5v-?K(8@G4 zL&&IxT@<-s0oeXkG>U;`AUo3z4qFDKnGC=n&v62TqfwZgnub6yEGLsdT)C*M8Y4pK zwzf9**3i^<*z)I)y^JmX9lvxvHh*`XGm8|sa)4!7jhmjF4n?4McmyJm7%vEXi$4&w za9sSwD9070r0_s}M>=s4wL{g07#43sl^YP1VTJgNJh!uDD48q;c zE{MhBI=k%btV5?fA9{4c1S8)9Z1v{Z$`u7mQW{!Vy2O4UsuQ3e!5)Djk3YX;SOds?Q zL!Z|d0ZH6PSk~20voq6dU$l>^#uaVql4TO>r?o#tde# z6ezCU0GrkN+qs7NO|yNpyxaj*AWFH|SCys2=%XpReLq)ns%ZVBc26)I@gp2lR$lZ# zA|`+cV(NiVj1s$i20WjibHs??#t_?$Mx)Lo6qcd?icUC-F&Rv^Bhly!X=!FzLmOeW zS|Jn)J+sha|s5W2^&2ja()PVR)te zijEM3C5$%}2)sSk(>b~0K+|bOr`;L6TDt)x^|I5GtX&zFbcA?`0@S=K&d1hU()FVY zA~-1v(TRrz}MZqO2lj$28Zm+IuXxrD^?4tgAj-6hz^FZUJ z=|JdU`Pxz$AC$P*c7;@S$z)2Ka|H4=#QBj!r@qaJbI~UxUObs;vur3WDwLfCL6AWv z9_QfB{Z2esVTm;w1d$gF`AeNpTvViD0Civ(2Cm+?MFD8inr_AM3U~c%wELpRqsQk& zX>tJxTHfkToPAp4gpQ(uCE#?}RfJP)SbPcTqK-U*j(!p79~^=KuNUmunH6U}wKw$o zR4(;7=7=1%X8>Qf7#&4&w$xonNjejh7Bn(8E+?lF17l3iamHlM|0xAXAkJ^i9KaY1vJ*wSI6k|>_&^PMN3=0v;O<{AIoPX{ zVe%^oamvhrL!^RAew{(4LWTnHTtq za5z%Xcl$c{z5O~fOx|v@*|v6cbm;FmTIo7Ky*~K#=+;0W_!Smmw^}SG&NtM%sk{c^ z+BeuU!EwBLDDbrtTn<4oRJ6l_;`;}W&cjUozFt}2&v80>3E;Jo1UAJzXiQ~974EUO&TjYLK{hHYhvy)T3 z(PaEZYsawcPn5^x`~F9A>PqxyfQlAcdG@_of*9g?zJjpKb+5}>PtDC{^MyzxQiY_h z{R@Rsbn}itF<9(gR#ujWsM(I4Sfll!X8`&0TA4zlZSP|%fCeouFLz-lens+0B?;ZJ zhz7j>by}H1&;MgzvG@aRoG&RYEv>-U(W1z)D1Uc%_umRn_kRKJUBmB- Skb=Sh0000Px*vPnciR9FeER%vWhRTMsNnWZ!BOg9E-SxV`m`~botnutUM6pS$uqsEY!7J@;N z(hy8TD>$go)+R`$ApzS$T+md)UxN~j7^9FDq_wrv23oq!bZ@7#zs+-B``*0QX-iRV zGIP&<&pr3t?>-9V7|zeXwuQyerxHjzDTx{|B{G|xd3hnl#aUgQ9P+`R6iBQDR9zaK z_F+N6uCSt?&rHu{6c&P~BuRP~Bv^(?YzU1BWm^02gmfMxDO-|Y1Jnem*oi zmytwKJ`5Z+p`hSsWpxit!3|M>-I&ztQ6*9oOz8M|Fmon=<>?o-K_rR7d@2O3OheNY zHK1YVMb4Q3mVYIQ!ZI`@+ij4T5Dz9J3pDZ^CqN(+f|2nF@cIJsViJg%^IBD7L=n2K zu8z6WJN_M({8?nLV~Kyoovy*+A6%T8M5Q}YfMFPoo1UET2cfgi1HoXJ7X-d?%#qMV#Sab?XxQ5379VmwaFB5P?H?mR&&o|P6cB_}7HxZr%>sS{2y@-4tpZ=0@Mk-jKqacnv)dl<$hd=TMzor@%jV6(+TN@Ai; zPMB~o^jeH?N-)&}bQwj7TQLzEktf;XVL_5z^&$75e{dAK`v(Cl1r$YjiLEux3@eu| z2BU!m&+r&B0p%(|cWg z@W9<41W7zXF^r?MaG%}G(&@o)_+(@2ecnAZDlbl)I6;I}*^|A<(6nf`*xslvExM`; zsA1$DP%H6cLP!d7-`AEdfpMC@aXnt}3R8eaFfxS%xZjA}b3aV-hnM z0u(pvA;D_>y>Ab8xJLFKEB`{#X=fU*)RKUq4w{cPCY(P zr6wmZMaxrhRqZ;F6B;to7eZ>HO+`4xhQXJR4C2Tm=;#LpT|K?f?RJAL*}k#XSyZpr z$8xdHF-OT!JG$|83zMUWo-J_~ViuhYN)qx64#|rX2}>KI=QwFH<9|#75(tF^?|VHS zxc8tFo}*RNmyQbR+7jPbe*iQ#*noPNdY^?ij& z*sK;y)uqys0wOO&xbE-i80I)$Z3=wt1V^GH`P${GRP6p`*&8+=!dkz|(2U+gCz&Xef=42hu(tU|rXWZKcx)HE8CfMS$iUY<}}RHT-Q$n%O%-J9_FG8G$3oVo4G z^5eNVIXUNXXZscTd}d7IFSOgP?q<5v^u>wZU^M)swWAs4nwEbQ8WUcN_Gk4y@URGr zZHJQt(a-bzMv9?txgFM$hq=t5A445M;r~DI2zFtVr)G(?6L+iAVflwymfhac(sJjq zBrwaukei#k2b=H`>b2kWgZPR6LThVl6Y;jyE}n;rU&Mgw8n|-;7|*x0wKXdX z&Dw;D!GD?YNZ!H4YtUC4zYDMGmiG4cf3z_ltqHZBbI=G*zvC;qRWZS2c$D30Zf=fw a5&s2MHp;dgJdrs70000Px(%t=H+R9Fe^R%>V+RTRGG&P=j3M5u4uq$AceN1Nhb91!&a?nezYVufUQBwM3U#Qak|wNe*y%{$hTl)9*KD!nh$0 zEYrL0utdB|N>=LzL*s)|ft~%k1BcvlH$7_ye4*%knGh$WlndR4-82CI#G~Hz%5nWM z;g;JppDM_Quh);M2;Sia2Vgz;JHh}a$d;7Wi;3iweZ;%hSvimpS3(G0M+Skx4kNU- z4u31T$|{jRPkWNF4c=hndE#j;*nyhhffZ818)OJ`*=e)|-Y33K4_ts-)ujyBcO;Wc zZX}x4f*iomdl-W~jLB3D;cIZNHupI)N^9RXf1IiCI`K?0?=#V;;_yfm_Q`0LEcUnWFqq6AW+k?h3DTYvEXG$6q=5`#QXjJET0K&YmH&Yw2k2g~MC)|;4B_Nw>c&iby&k?T&e%N8 zq{m9Klc=ku_2CcD;9(E*T#bvdvzV&;Y&q7+Z!kx#>I?dRN$X9%)k$)Cs$M@-mPk%) zzz||@vypn%wB=}V*c8{`EOtT$9D#suGW5nvSPzE@9Ho*2U)P%h+(!Jz6nKLNaEgr& zN@lfUo}UH|$8;SmF$$k!=V{0SN?KMo>T7m;Vh!l1|L)}fv#DtvsB7t{ z6w);ChQpy_hZp)Q$`?I)et4kTwB!jCr9zJeerFng#L$~A`GVmWP`?6O^JkAz)OB2M z7|w?LyCkOrweNjdYQ{#-;GDnTR<}Vo^HkkbYF>?96X)>ssUc+VDT=bHh-+sOrntvw zUDIRB@_BOBfQXImC2+flm;&p2+Zry9#mpz&;8+hI74bX2Nr*%Tlj5a(fJryY0lR+@ zq=^(5mmeCt17F*`1#lYkov>+4$;pbcMLXkS-6XXY|Z_2BVtJM&c6+k6#=|pi&N*$#lZAHzxcyju!(s-`8p zj*KpAjyx+Zcb%kfMB2}H%{d2f0tPHge&OU7t>R?6faw{>yi`ttz<_#lNp;VTr(%<` z{HUA`*e1Nqku}(O`>-HOr!hHFq-l?L>;9~djA`_9inCHcT1H#bK~+<#@#Ke7!-zK0 ztD!KDZ?@pJsc9qN#;XlR9}rwD=2DhelJ#X;(XH}7{ojqiUkW?DOO_$h1^@s607*qo IM6N<$f@b%9IRF3v literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/16/@2x/question_blue.png b/src/main/resources/com/rapidminer/resources/icons/16/@2x/question_blue.png new file mode 100644 index 0000000000000000000000000000000000000000..063ccbfeb24bbeb00b49011f96a3aa58b9f5d47f GIT binary patch literal 1255 zcmVPx(p-DtRR9FeER$FKsRT%!y%QhP!wb~}pBn?TLn9XK(=XN~bS!c2{J3F(h z{xFyG-_JS!`On9wNji3kCykZL1H_%9#Au%|yN$SbK#bWgCVNyTu*JYh%)=cZpp3ZwNKGJc~ zm3j%tJ)hUl#?8F2O`#|H64ckus5`|(0*uxSPIJ0LXXY)MSh8K;-WS2~fU@aCv9KrC zd+q=$Vyxi>k_AT;x#zD8B^u|=hTb0`2Fjy~}oinpb zr^T<_4p{?O`f9a9C7d4Z)k1>@wFj~)jqXWGb}+cByI3+04t}#Sh_Otddd?dkHC1fX z@!J!wz5H4Zr?2Ns8lN?3!{k2MgS#|ZDVDuu_X)+ob{w{}TKXJ(1szT{nV?q9I4|}) zg)D05+Xah0`rV|lX_G#_=5KnNdYOj1qAT-e*(?lw$1_^By+(Jqq$u^Xi&jl|qVI2P zjpy{O6+m!2+f#KX+-GmuoE6trUe;u9_lfEUSBnn4cf~);TCKaY)nIOL$WDu`iKB{T zKdJn4vBc>%l;4qIG_pG(XN?3vhA6F{m*W%AZOY?JRc|zD+S+TNQ=t=sDI&8X6M!`; z<)z8cwqNJmumso5qIuy~@rtJLKg&UmUn;SVmR{=zQ? zjbCqED|Mb27n@ZD6LqpU#!$QIEjx5SopxFaE^LyjVxoM3W?G_BsZ3lfy+`6ccr(ULV2qXc+4h#+En^e1BN4RKi`DDY_0CCw(rZo znlXvgNondA3IL?21A*b9DR{9Q>e1+vgB|q2{TbSkj_!J4o;8*6&aUqFl`48w#=?|b zUNHFR-ZYILNYlePqLDsEQ%5@J))tf4o=#kvdWe-|>Oileg_PWYwclWH$e!ElzZq3> zXmlW5YZm#hDhl6uOXg`P$pj==6zDZ*6&4&itET^E1aG=jkA%Pvqg*EOlMBnd%>#56 zt8m^mXcQdsXD5nOv^`UrTxImxZ#Bo#brSLgB8bS?QvAAv@2YU6aB7SaGS__`frbpbRcis9{z-0oj{jt1p4gk{S z{g*G6y>Cay{$OzU3{3!n{S#cGR{%(0IPh8}R67a|d+x&UppRI`SPcM3U^u|2po6e6 znhef4j#}8PYi*ix2m=5}U^u`iAQ^xOL)_<(B{gQ^VnJ_5dqsyslYuOb{sy-(0oOFV R1yTS2002ovPDHLkV1jW7S&aYy literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/16/@2x/sources.png b/src/main/resources/com/rapidminer/resources/icons/16/@2x/sources.png new file mode 100644 index 0000000000000000000000000000000000000000..fa72b21da8e9b9d227a99cc31f9bec409f3bdbd9 GIT binary patch literal 1164 zcmV;71atd|P)Px(MoC0LR9FecS8HrkMHK#Kb}wvsw57F6ZD~`CO--pA`~hv0M;nm<1`!QSg8>pW z{LsYLFM~!mG)?43jgQ3e!x$h@llDioff8u~6%$#k7AT>K!2l(-h4SbFx3t~edz~}w zl)c^i*sg25e`e;K@0{=4b7#(+%aG)=BiJ;rPmk0XG4=u&$_)brjG=(c#k^n4`(dDm zd5qR&Tsz$ytes58HFE=12b=Q8qp=SS=&uNZOgFsLGmJ9uZ+u?QfsSD9O(z@M05*T# zHh*|<NFU1XFChtccdIQse~Jhn6&N6+-&%aa!p(V|!td$l}HrIu zx)aGsBC0BJb6MN1At^~q^RuygT{(2!KtoFilTqE)4{>w4SZonQORp}tu04cK@R@9_cso;UvxDjZmzCWSF{VDBrg-M)>MeAc`Ehe zk(i4GK-@Eh$O=u-QCi&|!cpoqQFy-Z{1aW*2Hi}i^degN1pRjC!{R74y}cFwtOa%u zDX)9F3>8I>;MQ;$U;KF95gV^Gzy>II+W-bf#!#5E2%qh#L1xChc-xtIQSM^w+YpfJ zkAL_TV-wLCo!f-Cxj2tKcEFFCf5DZTLs;(5LgTLWpxQqB+5ciCe3}=>yRPEwm4Vr6 z=e6MG)Wg}K!V?j^bEE?ULt#8woQHFJG)kCd$HZnGe^VbKlF;IdPVzX(Y&}?5_DnCtocP|eAblGf>#ELQg^~nQut@0RB z_bP0{PIj03?qdIUKO-8`v1k1XR6hQw%u3_2-<-_sbm3{vsq2_pBh0HT0ZVJ9b4UE?h(IVjluxQ?_5~RpxUUL&T*F zOl58^O{q`wAenNjh~((hloCLu#mk--r=rhfXt79zoZ51gdgU_VE;tw5*-fpp0E84M z4P%IW&i*GLzX<`5kfO|4x};icmU^*tJRO90c<9%xo_x)Ib7f=N14qu literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/16/@2x/test_connection.png b/src/main/resources/com/rapidminer/resources/icons/16/@2x/test_connection.png new file mode 100644 index 0000000000000000000000000000000000000000..1f659a2d6530995cea50cfbd0cd4d987dddb46dd GIT binary patch literal 1024 zcmV+b1poVqP)Px&x=BPqR9Fe^R!vA$Q4~J+%`|@&5kx7JBB&%-D3X7hAi*}Z3c{e;BnY`Eqflo+ zw8+q8v}nL+vcez`f)>(3BNyRMqb^cGH@eV8iI$CHGtS?=ojdww-t*1887EuWg**4W zbH4NQ&N=Vg0r0r z+^VXP@}kk&XiO>3V(UH*epW<*okFLnT!v?61jDK#bcCXe{Klu`128|f=P?Er>I+`N zbPEyFyGtoD&@?!x^-j*UQU)a%X+1iVa6YDrPVa(z z$9OmZbI0>044^Qk!VU$D36vC1eo|K>psnw)uX9ikn2A90O{eZCwiGkKQn)zjc8_g4 zA;~}cz7z6}pl}Z)s-TARy4^4{ItZ@SA3%cvQ)y`_MZ5?1n9t|CJTWoxZM(ieh^8d8 z0e)^5?&NuKb_%|H=mqD>5_is3c~$u~;Xb3+>lez(%5EbqTjA21J73(Tj9O57P=-k0 zfjgg{97_O+T~5fI;o)Jc2o49p8F0q^5p})T$1_lPeX0t&~PEJ@UNP*n538~cO&t$H^BUu3W z=i)uy<6U8N0)%UIMTTl_ZibSQ62OLop`jrtC@28VldjnX4Y2h9P=9b<@fAF{S#`tG z*}cqwHsCd^kV_45ng)Y17Jp+w5MX+GI^?{fq5@1N-iMLp^#ju#HfE5MV~es3a0u3g zwbWj3)M)mdz=v13OAR{9K8XFtI>-9ogTblyQopYwF2^A=0n4k5e zfoG$f0m;UU9N1K1mH1~Rjnkjsy;fv_^Lu)FLP6Bj)WFn~;%|X_WMm{Xf#asQrTQu# z9&fl&an*O)({Y4f#OT-cDf@qU)||&gnRxc~^?@6|kSNYMB>c@PN+23gI}OI8*RalK z^*U{~b7i>*%NCkEqK?5}fc*S?$j!|aS>cmo`BW4caWfzSrDs$11vfqqiaeZfbx614 z4wcksG(uKZ7C4%2J(l3UZsf763O^ uSXh{*)9KzKS%llbdAL+BqfN+O{`~`1v07qpeG`rV0000Px%14%?dR5%gEQZZ;$K@fejfA9Z)XJYQW6EAnMP!uFVuoSJ-LeawR3JVQlKr4eO zl12HDa_>L5SdCtwak8Ei58I5f$R)AD4)^7~jZ4F9c(AgK0-VYJ2dyf=ar<;8-Zm-ESO&O)+Re$3ry^uLt^N?UCCT62;% zn21Kr#IBi{B`o(xBx19b3KUN96*#R99*vLP1c|*6;;PxqD{})7MMi{$5K79#lIwa? zDZk_R(|hHUSb_#)mI}A4sTzzLsVosbMCW=QA9@bw#rwIE;kKL3=Js?IA5NEOwY?)A zLlw)Y$Qr3hX4>iq=Us;@28;LWhczR&IVtuxR`nDsK0WjO>k*TDrd`0iyG;kXC4&wK ziK8cWWqK93#G22WRhq&G9S098(~FY~b$wkjkVFcd(9#T{tRsH*WM=-G31Si`8Re`Q zX4%*iMl5I8k@W&}2`e$T_7nX(!44&K)+74z<7{_5cBJms3n9XS)H+xh=$VbVe{y~S XM~}2WD&Qhf00000NkvXXu0mjfVFv^# literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/16/academy_page.png b/src/main/resources/com/rapidminer/resources/icons/16/academy_page.png new file mode 100644 index 0000000000000000000000000000000000000000..ed1c638c1747ac731a4d9cc2fc14650889d62972 GIT binary patch literal 475 zcmV<10VMv3P)Px$l}SWFR5%gEQawupK@gqUZ0;i(yog|;onWCsu=fv$jh%%cq_MD4NuiAxkbsRQ zg^eIqc6J8;fLK^)VPz!>N^*h$O-Rgncf0N#>~S&CVCBNz%)a+#c5Y^Ivv{-tKP=NM zXKdRUR8;kbpo>XT)spm8AC6S@NqRrDSIgW|1TQo4}DEaZq}8L(w>p?7Jp zqlzl&HtTRJD}f*ZGMJWNFUuS@Dm6g@j(kjly)0)=&?hRzh?o-{+Z8>FO026zeLnNr+w3y!qBvU}hj+o4q*d`!4A_*sixRgfgilSb`wEnsCNS#zZ%%zc7m<#zSQNg&^gSKE#@MbYz zSO0W7IG^x7I>aCj3=4Oyj`9MLaMmZZUw<=bVmRn8{{JVSm8O06my*A0zW_nDdKJpr RscZlM002ovPDHLkV1m^Q&w2m= literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/16/academy_video.png b/src/main/resources/com/rapidminer/resources/icons/16/academy_video.png new file mode 100644 index 0000000000000000000000000000000000000000..f4298abc8901eb8b46508fd7cca976db0c0e8b5a GIT binary patch literal 527 zcmV+q0`UEbP)Px$$w@>(R5%f1U>In?STSKvIW9-Mc=0UZ_wPSKZ0zi3nHc{vN-(|UkYIVkDbDwka$9)a;e^zYw48NPn{h%5wVb8zrxZr1LXU@T^6V1mj1 zWB8W}v=C$|9J8{q!=zE@KmUIg!vx?4F#cmOfC&f7YC2m)_GI>89p=oWBAYT zgau|BFuL6TXjnM@*D&vu;No(gWy1LF|NsBsSYia)3R4HCIT-jDzA_lGTzmap@8b`U zMPR_j#K>4G#{K&pkk^A%BUB-fk_5UymF3&-e}Zqm{ss&4vNOLH=Hdvt6d-_-9uRtv zSey)847?2gz+PYm`caUJWzx$iv0?(Y!cAZ-V)zc`NG^R7B)ICG99)Lp3;;<4ki2oI RkO%+(002ovPDHLkV1k6I_T&Hn literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/16/gearwheel_left.png b/src/main/resources/com/rapidminer/resources/icons/16/gearwheel_left.png new file mode 100644 index 0000000000000000000000000000000000000000..b163a9952965f31a40724fe8bd44ee81d9c407ff GIT binary patch literal 813 zcmV+|1JeA7P)Px%=Sf6CR5%fJQe8-sQ5b&Daoe{aT~2ed^{1v}qIuzkb{iC31V&y4No6T^VRjLk zWQ!~<1ApqGa$-b(NYsVUg%l|S{*a+2rM5|E>M-Y~Zu|E6?K`J)W}6G@g~NHy`##U} zzUOcd#6<1Au53_3ZvjwmfnG9e!@#rJPjpRJFfMxiQ-7{H2RRw$$c(DIsA zds0ZDMJ#P1Kn}$6Vlm=}D{b~N*40S~J(_4VTs9|}K(7;ELzcn2=7WV_o|!;Etn!Qg zVx(3-?Wn)hA2CtYw09mM3h7{7VYej(EGfz18W-zzT8P){z@XP=y!$xR!*e>VwwMvj zQ3EXqgAxi3#2a)6xmXNJ;Fxp$5QD)dDy+(}T9Qg6Nk;iO`{2{q1l0(du85fL;Vaa( z*pKhoVSbaEY=X&|InSz0_cm3Y^l-v?+nvGOtgJ$ghepR&pEX}gnVhQqEKULO~aRpn{ByK16FGVO4CG14(ALyp|I&;_qHtpD&d1- zj3E@-K>k1=R}KWKBXt#}xDp{+w6eNZmYSTXx0p?)v2Tv0Gj)x9YLz;3e`ZEH?=H&{ zXa&KI<5W}FkZ?O<5Pk!GsBN`BUWDX)<<>KixDy3 z!v?nQZjINuG{4|-ZBN^21*;_$FhWcU;PVGy{Oct6*Ss(@+_x@EQUXHg@$~fcX>PFj z6H`)%_w#otPKCu~?^=Uv*?l1(idN=MjmCr~CFnow9O^3wZAb}B)63S4?Ck6`LWmr_ z0(Z07Y?v)_?)u#-k9(z6qtSRqdS2vGO2^sS{9&B96+iznh~#bTW#{XgPvHC}9 literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/16/gearwheel_right.png b/src/main/resources/com/rapidminer/resources/icons/16/gearwheel_right.png new file mode 100644 index 0000000000000000000000000000000000000000..61d29c3946f6d13f31a46992beb0f913420d6237 GIT binary patch literal 796 zcmV+%1LOROP)Px%)=5M`R5%fBQ(Z`tQ5b&D=l-YDxtwf8r8$5b6CR1vkdyn=4>PAv}%a+JgjYmVd+n3o@Jwrk!Yk6 zDV6=*t(Qh+5k+%X&oN1qy2(`7Ysmpijw#WjWSv$6={hawb(;KSnRQaQ50D%*;O3JqtYTfe3<%zRcckAOr}1ug0s_m zV&87_P_D@cGxLj~bsisRcbpDUK%MK(c(Kh^PUDHm>GdZa*Rp6V8{_nNYcPUNty-0+ zMWWn9y+e_J;$R{{g6qUX)=N z5QI32uxv5UvQ2VdfidGi2v@AGZ`9_RvUC=+(Kz+pw|u6h?X6OwEGR6<&!f|Mo&$|q zy~3m^elYrs+p<974QS$Ydmj*idW0B|ForZTQIasZASTlhjD4I0gUM)p)Y9}@CR3jf za19Ko*5;P|OM$?y{d=rnwd4YW5TOEuBP>jRn}KL}6Ml}3utGepBjw(*va-6t!9g*N zsvLDVP~fZYJo)03ydb&!fncF;;aA3@e+h>-H#>MPMpl)qx^zQNDB!mfN<}ajw3n2W zJWsAbgy5%P{^Rk1`bi0000T literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/16/injection2.png b/src/main/resources/com/rapidminer/resources/icons/16/injection2.png new file mode 100644 index 0000000000000000000000000000000000000000..c2b495acd070a57724aa45c76da1b648e3b4fdd8 GIT binary patch literal 616 zcmV-u0+;=XP)Px%B1uF+R5%f>QcY+RK@@&(b~lYJ1VN!FrH6Q}f*`Kxp$%0k3KpTYnAnqo9t05! z9*jS0Ye7)37e%x;Yt%q{u^v1~V+#p_mlnlKy%?$}m9)azHaj!Vnb1vB-G~R@VdvX7 z-}k*YGY{b(qVv|v2G=d_v_Q^fo#aBbgjN3p;n>wD`B>Ol|BXWE~~q_Z8j(VDwq0Ra8gs zZP^xm|KQ9X&ll^pVo;dj{izI|MwU-fRRBKXjUjY?&SK zsZ3&1yvJ1|deWuR(dM@929k0Z2{$7%4~`DBd=1c+QyX95O1e*#zM}?ct2+M=2F8i} zjpGxw#5%vhKqrnk&+ET~8>I;Qn7`0l^X!`YU*Z?%#medu{t24^0000Px%97#k$R5%fBQ%h?UK@k4BcV>3SjgL(Z*~Dc}ijswZAfCiS4!#f(f(P%Sco4xK zV9+1nQ4#$Gkz70|D0mVSK?6xPK}|%03KE_5k?c&*tL&QH*_}zYp{IMQ>Z`8)>T7{* zWBQe91aWj(fKGx$J#hwZ|9xeKTR{O5AuBb?V=EcP+Ah#-wXy93-AqAm}X=mo21(+oPOzR_T#K8x8GAV zo08?3D6H#B!Dnk0UQF3|f2NEv-O?RHw{dsLye$)LYk6`&3g-RycBH=o3ap2nRaDF7 z(ue8Flb)1UJ~{KtKiavgaH2tDpKUs|(a}0fIjM~w25Y-IWGzCMHVBm1w2xy_Fxz^s wF$Eo(Yn^9>u}HYjY-hHaFupUFoyPL|AJsYbDZ6*pR{#J207*qoM6N<$f`YLX+5i9m literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/16/sources.png b/src/main/resources/com/rapidminer/resources/icons/16/sources.png new file mode 100644 index 0000000000000000000000000000000000000000..57456b963a3021fd238451aa6b7affed6d9abf21 GIT binary patch literal 530 zcmV+t0`2{YP)Px$%t=H+R5%f1WIzMfwac{s{9{OAVEFIG$jB%MlK%hy|04zl#$7B-40Db)M_onM z!U*GpHmqWM^yhaUke|u$pMeP`jzTjs`~&i*%dxN)t!@bW1(E|B0MYzsJJ2d86b-mI zjEwu_SXewkhJbAX+f1xxkfA`eP}edrg8lse|0SHxMsh{Cr83#b5@QsxWY{F*B?=)3sx+~&&81Gs?X58_5?8OQ1pX2 zOprtZk&qYVW7t|A$e=AF3|CndV8*cQ=naMkuRp=XU<{C6pbHpx!33^9eaBG0`Zz;( zj6H+A2p@x&p(2A24=2Nt!`EO+&}hb8SfkNHUx6Xl!w~3i7KZfMyBY4h_<*Jn$c08D z^W#HnU(479ivfkqV5;uC{J`+)(>I0-_g^!dyz?BZ-+=~9JKhq#kOia(l(rxJ`J;)P zl=obI1Tznlh9u>plRz18EP&E9C~X5xoeFXqrVgkGNDin5mJdLpsBs1qAT>J!0P5DS Uov+;NzyJUM07*qoM6N<$f|bbW3jhEB literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/16/test_connection.png b/src/main/resources/com/rapidminer/resources/icons/16/test_connection.png new file mode 100644 index 0000000000000000000000000000000000000000..b78b7f2de5a7f530e3f93a114edcb17d5e13da67 GIT binary patch literal 526 zcmV+p0`dKcP)Px$$Vo&&R5%gEQaeavK@`0+^Ne5-I}xqSme%gZfcPuKM$u{;S#&=jLW~5#M9>I+ z3I&6e;3tKcD#fCerLv?_u?UN(%kH8Ov@sw&Mc;eZdk3B*ffze4%)4{$Id|Tf55Rke zC}kkByw;BZn@WiRS0hlJ&W;YAyO^_)4<_P?CP4d#5YPySb;MQMHtx6@A)HB?&%2oO zi-SmfeFhPy^2y?P=Z8bHz2WfTrH=SVJBY+LKOx$$Hgl78FLQWj-M9;&5*H#8--yy) zM**E0mnfBmkdq6cuLWB$5S_VF4R5K7iRJY*5`F=|ic$^#GRJUrmI2$c$Sjybh{bd| zJ;QqSYj0<_rMrg@vz-gY!Oj1FpU@5+rda2p(e}BwzWHFgJ}0?aiE4$e?k~@aga6DC z4u>I;NI*0i)%YkQyYTjLOD_-`8_dZH)k@ZWYBujfwlzv7laNZKz~}R6{O|~9d{i}h z0j|uA3|%gSzV%4~wNlCsGP_8AEoB%s^Vw_`{C+?vT25z&TL2YS2 QWdHyG07*qoM6N<$f;08&oB#j- literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/24/@2x/gearwheel_left.png b/src/main/resources/com/rapidminer/resources/icons/24/@2x/gearwheel_left.png new file mode 100644 index 0000000000000000000000000000000000000000..4718e726e77a0e994022a87a12fe0d0e3b44aec8 GIT binary patch literal 2901 zcmV-b3##;qP)Px=4@pEpRA>d&Sq*Sh)fGPP?c2?6lHL3gB7YGB36Y?6Y@JT)bX4l}$C>`oW?BcV z+D;?I5wR#qG`N5iumVXz%TEF=E#gEvW5*7)9d*!wsq(8t1QHEs$p7wUck{FV@AbPc zdCPlm-!9p}==5gZzI)F(_uO;tIrp4%UpPv$)ok7K3*usrlEmFg9Qg#|?s@Llv zD^1+C?OzrQLsvkP@4@0%8uMKW2Y$nGQfr2g^R9DQ)v$uC+uHn~L}D{@7mLMAGp80( zey*K(#Ku%j6h-Ryc&WSJLy>4yCdHx$GjfF`&W9cfGJsQ(c<&3_zZDz_E#U>rYcD0lRnmcPc<-ldGr40;v>C)vs>Kkyu#r%e>%w}`6+&(SG>8u_#Bv1im`?HJ* z*LXk$*L0bi%$q&ynuTXf3v+X6I;@yXct#V6M1;#xZw>2Tt;q^}JTNi^*2_#reACQg z%FeVVLu0`xxdwgIJ>aIE0S|e+eu~Co3G`Tl+`vVEyRGu(1iTs$6=Mt3<79mkLmWQz zkKsGN{{s=eez%^mAlD)Ly&lfSfl!z_I(z71S1%3vhY@i`$%m-p@eNV$z#s)f5m|AQ z$;8~7To8UFAfH>x$gof-6r_>SKmu#CAOswl=ARrt{FWyHsRP8%0E5>yG_6J)U!%~l zb9zg8DOnM9^o{LgM`tfWq=<9Qxq&~3=QQGckQ2F5k;ETiyugg`ItOP5hczy|!b{*e zdguKU%20kY;j8uHubmd+*#Q9|9=26cS)Id%7qO%((o=x{i0 z+vQw&UQL&(pP&FEh9z1dTxHpfGbqoIt(ni_`UmGb#$!6yxN*g=AJ-7*2FuoN`FWq) z{ql_BBCce{3>{WJuypK$Gcw#^w=dqazIJ~CQ3`-z7J~2`i#$dllO-iNA#vP=qUny> z6uf2Bi>se_B|AI&zn6RZ;9ZH$X3dBL&1OFFK*X`eodgA(FScZ3q4EriV3@2t-tput z@oS!_s+!P(xrNsIP{UHKf_Z6945nA zqlg@=S*^lbh6Dz{h7~Kk8CI*yHK=6aD@5!V*|WwX_-G*T%SYC2uD*i3dQVoMe%sy! zD24wjMx;-0GUZE)fasxbC?|-O1UR_J*-Kn{e5$dD=e&WSvB7M%u^fX0W^oO;cPxHt zowwTg!fF->Qt@Os9)fRyPoII{i`mXA6Iim*Hx6FRVmfF!+I1kUYatH5-DWjThC$>L zbqfXqIc|?<%_pnY4*k~oVzC;h?;{c$mk?3JZKIZD8#{DJLU$SAjbP5bd#&Mcw5TvI z&j?LJ!||e5gAWEnnUUDg{TiTdz{3X(w`<7-Bx!s>1)_bks(362YvJ-GWDp5U--_~biQNYfYHta3`<~U~ z_2Wm^zwjNe&-ddSiV87DkHxJftaTqfg@rS{sfVe@&|i8<4N&(7Q7-5k5(Ldbc4qH> zm%DDss(hIi>oWnLMSZ&+wWpZ0;73{FH*vg& zGjV;qU|O|jUCoCwL6RS%C<*?s!schT~Y(9GJ5}fjcatWH zs;VlsZErA6pLO-gUl)voXLz_FB9=rvD+%zWB8tA8@EFpdNhrZjwSFvPnCnbdc*Y>9 z0#0N>oPqlo5zD|wPqxaJkEAAJgcE2nxZ1JP$eZ|M4WXKUxc>PE<-sI=lFIXRL-V4T zB(h5ixjb(2`ovZiaA5A6;lqKu3m$`p+Htw;PtMNI;GO4z{c4-7dP^(=VS9&8p*rVb1^)oSJ0UAr0w#;+bF?%o+n$gwM&*9PY)r?V_2eFGEMZ?kg;YirVki+gi>Ns%}*G%IO z3zCoVeQ4p?vuArX%ya|8L;T$GhuQ_6-^!{_bwhXz*zK8%{<7(r`TL%)pEYG>@$4*H zX38%I_N?ssS2nM;?c4Co*9uDR$iubqUH?F@)PbE(COmfj3~q6ET}^l4$9vl zM2?`%YU^}3vX<|1I^$oLQ8GLUlP6v<(vjAH_Nr&VV9umLWo2b|V)7waBjqh?q6@Gy z@P#VC1gog1_&TaqRwQbolS=3ithE%vT~xsgyP)a%!WCeW%$qmwb{y#cz!Ld}k`Z+x z@!f+w@JSMU9Q>syz*dJl-HO+nP@*s{jjDs)?Z$8+%82tSa6lTVl!*dueSDA|4mu3M)DCCE72>Lb?Sb?eEu!IY^dr?k*poS~`>mHXC#uI(J z(FzI*UW1@zc+7Zw>IZxx80UfPpA+Tze?|8{@@bT5m}=5g00000NkvXXu0mjf#0!k` literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/24/@2x/gearwheel_right.png b/src/main/resources/com/rapidminer/resources/icons/24/@2x/gearwheel_right.png new file mode 100644 index 0000000000000000000000000000000000000000..8c194a0140f673dcd65c1e43e86d333eecc44eb1 GIT binary patch literal 2929 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 b*ZzM2opF>qz))o?00000NkvXXu0mjftbeo4 literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/24/@2x/injection2.png b/src/main/resources/com/rapidminer/resources/icons/24/@2x/injection2.png new file mode 100644 index 0000000000000000000000000000000000000000..6b2b5f103f5c9d992ca608a86ea8bd162f8f1f51 GIT binary patch literal 2141 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/24/@2x/sources.png b/src/main/resources/com/rapidminer/resources/icons/24/@2x/sources.png new file mode 100644 index 0000000000000000000000000000000000000000..9a55a9d5b9abcf1946f5a47a48776b929a7be6ae GIT binary patch literal 1662 zcmV-^27&pBP)Px*I7vi7RA>d&T5D`nMHK$->@IDWBDSr9os=A#9YOS|!gwdr4G2_7MXG)9+xLpM6%vibUS!ikz(X=&qhLyy! zB?hh;6vG*ijH>pz0bkoT3+1_z)pNfHBs8D`Y&NVt`RwRGOl{ppdrKV@F*cAn-KK!P zVBH4^WP6xRuy^AUF^=KQ7q;ITU{|Son>!9XE0)CA7^AP2ehNrG-)64T7e>?b#I*ad z`?VPul#(fS{OQ#q_IqZR@(?%+>HuidF zKo1Yv*K`_VPlEwhHt@M!-Yn3UcNwYTcAW=GJ7*-#i`7kmIWq-dp%Sn~C}aJhU`PzG1! z&Cnzfb|JF|p|OX|wP^AXB;Mo1?`JEprQ~?X<1lvNO?`wsj5Wp)eY+uR=mYrA-GIDp z2gm@d@%vFz+Qu77_;s{W#o57*<*=6}dv(Rk5&ht<*G)$a%XV}f z7(1^4(+>DVd8G_>-+O)>IDRo(c;?=mkwKyqe75&6{=RhGENgH&!cH3*OwZ+NT6BM& zzl`qjv0x>fQ3f%4GqDREPe(G%+6h-B){_E}DvX^rGG&k10oilpY$(FniYoL=io-ke z#=v0TaO0UzPhU0epkur9{Sz^dUtAD3ajpOx2>kBDvslm`b_aS%o zBV-zZsad%dGe<(uO50fc7mm>kM63{I#xC>}w^Cqb;PPuVSiF7@s!17+CYMYJoN;_< zd>VRp?Tpi8^PdzRvLeC+tx~|)<@|Gs+XROn$*F&@}^O(qn{;=wEx;QTyUf$9Cg1ElOs1y*g@CqXWlm`*!vAK7IilVQ`blL>vb z{}>LF-;ZQrtc;n*l)KWoAGg&dKQ^15weeO3ouLKIN5D*qE=Q+!$MJtEF(EAllLqy| z1=?wse_4!3E?mZ^K;-?jYOXYUuOH;gg-h011(^13O0WBwkY{2z53ec8%bt3B|1*|AcLR(opaR?>Z$#*3 zgd9rnG#L}OQjxNQ+|IDrfF|UN=T5)mYf%AK=1#t5H8dgLS>S7(lyp0Vm6%WMF5kWg zKl4TRe3sSxD#=KWsTuHPzH2`<@Ku7J@7j+&nk&Nf_Wa!Df8GynQBU7QMgRZ+07*qo IM6N<$f{*4W>;M1& literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/24/@2x/test_connection.png b/src/main/resources/com/rapidminer/resources/icons/24/@2x/test_connection.png new file mode 100644 index 0000000000000000000000000000000000000000..b85de2bbaa51e5821ae431f3b893a0004c0a0a76 GIT binary patch literal 1586 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/24/injection2.png b/src/main/resources/com/rapidminer/resources/icons/24/injection2.png new file mode 100644 index 0000000000000000000000000000000000000000..e0db8885e0096a7e84e8efd6c7751622d3a6b20d GIT binary patch literal 983 zcmV;|11S87P)Px&kx4{BR7efYRb6OYRS=%JKTXn~L`(4@+Crrmgd%OxCYykyLR;;FV9T!E*cX*z zedt5UljefAbOS8}E3^-7MaWBjy79qMC{g^Wsb*JE!4q@+Y!G4A~tzq#3a zn{8~^2!aE9X3m^B-*;xtoV!H-vGBja7ZFZ5{T)R7A~DsI(D%f~+r`}O%lG0fnxNhN z%$o5hdO`A11sGFk2p`;TXLGL0VZ3I%8v!BY^}T z2Dk6@Dp1IiqZ?$+(Qe_W%|Y0MvOE>}m`@hE zv!juXVZnEh%H*mBKAud)L;F>aQyQSb}eu%+U|6_@Uy@6v$ z`GA7sQjaw})NnQ=oV*OqWE_ut666&eG2>+OGxvfk!MqZsy85$tk^Q=e<*_xk)pp$v z-Vq63k9`e)`R(TNRpxL0^pmD#O^&>kz6=s{@qDQuXmj#S0r_J%`EpQC*O0x*v;3OS zli810*;gX}4Rq9d#Y5Sz=0jF`DoO-!oD|>?gZSlNn7#P6 z&2~*C-n>~tkaF|uu<==qKw2jXxsDIi9_`-svt3vFXbsy4ZRA47L7JgV7HATAaSY=iGBq&*R_gHF#es0~McmPx&Q%OWYR7ee#RZU1#Q4~J+zWH+;r%_@s6QVTx6JoGM3z4ElROC=u^al}Cs|X@m zDM%YzREyRvf{5CLrr@p+%!N>7LYi7)Od@INs5Ni?-n`p6Z+tUv-g}PIfnn}F=R4oI z@7#0FWl%&;56iWgqzDUu6O6Gs5KyDR8O#YGrWruL34r9SOl8mUgcW9Ru|0m_AHl_C4AN)BapNZZ zPQHcYJ^xEi9OOg3q2A~tBWLal)=p031wjeya{x3|NwCGkfHMa32^B`7DlDb&b__E0 zI}THe39&8U>6$sWxr0nvfNaj$gx$#x1tqv%=K`NIcNUE@r3!fYJq_KniniCtF#w4q zF|jjH-Zb7;Iij7GF4T^EdpU!f4OVg@ad8G$cDNu^$-ziYwoFE_rDXC2tvQts1;iX9Za%)el_5>UCnh|0oC;h@%A5P3HBC2rM{d(01 zx2%Ic=fuxT)qAvAglX-hd9I_(@NPLIXteC#&nqCjuYWTVx{UEPuM!8YBR|8CTb zge&jRV?m#&14{iTAM!PlldtVb_r8jTXr9x^rKwmn2v$qb>zJgR>uE9Ki#pRhr;$rj vkp>sd7ILNzr6!^}r&9bJ4ty|@8rS~-x6XU)Rxyzn00000NkvXXu0mjfJC?C0 literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/24/sources.png b/src/main/resources/com/rapidminer/resources/icons/24/sources.png new file mode 100644 index 0000000000000000000000000000000000000000..3f4b34bef311076a34107deaab8e1ad1ac6a016c GIT binary patch literal 758 zcmVPx%ut`KgR7efAR$E9^K^XpK)=Nf;h+%As-Ls6Kh}`bJ1W}OKQXSP;sr3_>3i5=tV%E(BpHmC!?hW#MhlG~a2@arf-3YnJC_X8!;Czwe)! ze`b!r6kmDhPT}apScOv79tE*f2>1zC3WEZwPYBtTnU>Mo847+%Mk7p)EupYKGOp^i zuSP-nOc=)|AR@Zo=t~#pxTbfVe*3n(K+4TnPy#p?c z)iozQbCI;`JVhzGQHD;1<+#$?W5knN@`M__ zDU3cTP#X;3+T)k_Iy_=TlUq8+--wvPcyQ|~4E-F%!xtZtqR2g?nlH}J0R=AIdzO^k zap*lj!u_#xZ7~i6R)Evtr?T}~oS oOAN7$iOqb{@AM4q|M+eB4LZ;b;P9ywUH||907*qoM6N<$f(q$t^Z)<= literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/24/test_connection.png b/src/main/resources/com/rapidminer/resources/icons/24/test_connection.png new file mode 100644 index 0000000000000000000000000000000000000000..d49b97d45fc15dc14e8c959591a74ed6f97f1e05 GIT binary patch literal 833 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 xj)d?~MNsJ%;d;zY*gia93D4L`RbDwR`{o literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/32/@2x/gearwheel_left.png b/src/main/resources/com/rapidminer/resources/icons/32/@2x/gearwheel_left.png new file mode 100644 index 0000000000000000000000000000000000000000..6be8b471974d53d395cf16c64eb342835f492707 GIT binary patch literal 3975 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 TYG#eL00000NkvXXu0mjfy3az) literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/32/@2x/plug.png b/src/main/resources/com/rapidminer/resources/icons/32/@2x/plug.png new file mode 100644 index 0000000000000000000000000000000000000000..857bcc41a66000dffb6a2e832ddd643f91250efc GIT binary patch literal 1133 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!SkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?skwBzpw;GB8xBF)%c=FfjZA3N^f7U???UV0e|lz+g3lfkC`r&aOZkpg!LK zpAc7|g7DmO|D+tRw8F~X89-FkHxt4L%dG&Skess6oN{CY6a;Z|Q3WAtvO$7jIprTO z+I_eHM7EzU*+KMzMQnj0KrVy?GT!eo10oP(w;2!t8Sj7$msDfB$~H z`M0C=y3!FhE7o<3|A9es@23Olp?n!nwYeVe5BU4*SW>3egxPdov%kTJ>I-6c+YYE8#nZ(x zB;xSfE8*eI1`-VqSLm)Syb`-TG^X_Ys*Y=Wzu&7a`}04#S-?v}&S_s<#ldRteeE)b zIGyV1{_k-JnAF6`ATY^8kwM^ELxJXGb%$TP#)Umor-p~Pty<;j`7@T>V!^l^X82H~zB_H%Q2zAa`Hj z8B^QaUWo%5V$8>m@g_70&q$d)YuS8-Q=ET3a;pdxN;JqmW=ZoUbYPAu zyMy9c>t!0hjKv+Qr@xx9k3W=^nR{9bLpI+5B~F%%#>NyO4uvf&tkWzTMYmlweh9*`9mR3d<+6G2I4bKDicmXvqc)I$ztaD0e0stH+*IWPq literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/32/@2x/question_blue.png b/src/main/resources/com/rapidminer/resources/icons/32/@2x/question_blue.png new file mode 100644 index 0000000000000000000000000000000000000000..b0fbf3a27ef3111ffe14437e29c21f57af6a76cc GIT binary patch literal 2592 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 LNkvXXu0mjfeMH_m literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/32/gearwheel_left.png b/src/main/resources/com/rapidminer/resources/icons/32/gearwheel_left.png new file mode 100644 index 0000000000000000000000000000000000000000..546b7716efe6f443616b4d9a1d5863b3b3201778 GIT binary patch literal 1776 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 IM6N<$f@b%9IRF3v literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/32/plug.png b/src/main/resources/com/rapidminer/resources/icons/32/plug.png new file mode 100644 index 0000000000000000000000000000000000000000..bf5a8d3f10c0a3d01e557653bf9b53e39ceeaf65 GIT binary patch literal 723 zcmV;^0xbQBP)* z7zLvtFcd?evTx4$s)=*|S5BC7en8w`**EJy)B*oP@~ZxX6x3kiysG~cJ1#i4QUHic zLh~yR0_oRb1vOuQ7JjUqG8gQC+9?bE1I4gnprgolKyZFFBM|ojEyZd$!U1!s?*O2s z5HL^*&^e#Yi7Q2s~OS2c0Y|MI?>7?>yrq||o)Ppj|#@12}SGzO_h);DFvrvDR{ zuE)ScIRF%NAYZyfr4Wrl>XG&3wDkSYY3#9Bk1?XjJvLp)B_`!6zNjEN1diyO_kaFalF{BP4Z0 zf)^M)UW_6xD*k*p5b-a8ypQ1vIQxc)H7nnyK^h5%X!0049^KX}7kY})_;03>up zSad^gaCvfRXJ~W)LvL_-a%pF1bRaS?F*P7bPD4*pQy@oeVn}+1-8KLK002ovPDHLk FV1nraKM()_ literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/32/question_blue.png b/src/main/resources/com/rapidminer/resources/icons/32/question_blue.png new file mode 100644 index 0000000000000000000000000000000000000000..063ccbfeb24bbeb00b49011f96a3aa58b9f5d47f GIT binary patch literal 1255 zcmVPx(p-DtRR9FeER$FKsRT%!y%QhP!wb~}pBn?TLn9XK(=XN~bS!c2{J3F(h z{xFyG-_JS!`On9wNji3kCykZL1H_%9#Au%|yN$SbK#bWgCVNyTu*JYh%)=cZpp3ZwNKGJc~ zm3j%tJ)hUl#?8F2O`#|H64ckus5`|(0*uxSPIJ0LXXY)MSh8K;-WS2~fU@aCv9KrC zd+q=$Vyxi>k_AT;x#zD8B^u|=hTb0`2Fjy~}oinpb zr^T<_4p{?O`f9a9C7d4Z)k1>@wFj~)jqXWGb}+cByI3+04t}#Sh_Otddd?dkHC1fX z@!J!wz5H4Zr?2Ns8lN?3!{k2MgS#|ZDVDuu_X)+ob{w{}TKXJ(1szT{nV?q9I4|}) zg)D05+Xah0`rV|lX_G#_=5KnNdYOj1qAT-e*(?lw$1_^By+(Jqq$u^Xi&jl|qVI2P zjpy{O6+m!2+f#KX+-GmuoE6trUe;u9_lfEUSBnn4cf~);TCKaY)nIOL$WDu`iKB{T zKdJn4vBc>%l;4qIG_pG(XN?3vhA6F{m*W%AZOY?JRc|zD+S+TNQ=t=sDI&8X6M!`; z<)z8cwqNJmumso5qIuy~@rtJLKg&UmUn;SVmR{=zQ? zjbCqED|Mb27n@ZD6LqpU#!$QIEjx5SopxFaE^LyjVxoM3W?G_BsZ3lfy+`6ccr(ULV2qXc+4h#+En^e1BN4RKi`DDY_0CCw(rZo znlXvgNondA3IL?21A*b9DR{9Q>e1+vgB|q2{TbSkj_!J4o;8*6&aUqFl`48w#=?|b zUNHFR-ZYILNYlePqLDsEQ%5@J))tf4o=#kvdWe-|>Oileg_PWYwclWH$e!ElzZq3> zXmlW5YZm#hDhl6uOXg`P$pj==6zDZ*6&4&itET^E1aG=jkA%Pvqg*EOlMBnd%>#56 zt8m^mXcQdsXD5nOv^`UrTxImxZ#Bo#brSLgB8bS?QvAAv@2YU6aB7SaGS__`frbpbRcis9{z-0oj{jt1p4gk{S z{g*G6y>Cay{$OzU3{3!n{S#cGR{%(0IPh8}R67a|d+x&UppRI`SPcM3U^u|2po6e6 znhef4j#}8PYi*ix2m=5}U^u`iAQ^xOL)_<(B{gQ^VnJ_5dqsyslYuOb{sy-(0oOFV R1yTS2002ovPDHLkV1jW7S&aYy literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/32/sources.png b/src/main/resources/com/rapidminer/resources/icons/32/sources.png new file mode 100644 index 0000000000000000000000000000000000000000..fa72b21da8e9b9d227a99cc31f9bec409f3bdbd9 GIT binary patch literal 1164 zcmV;71atd|P)Px(MoC0LR9FecS8HrkMHK#Kb}wvsw57F6ZD~`CO--pA`~hv0M;nm<1`!QSg8>pW z{LsYLFM~!mG)?43jgQ3e!x$h@llDioff8u~6%$#k7AT>K!2l(-h4SbFx3t~edz~}w zl)c^i*sg25e`e;K@0{=4b7#(+%aG)=BiJ;rPmk0XG4=u&$_)brjG=(c#k^n4`(dDm zd5qR&Tsz$ytes58HFE=12b=Q8qp=SS=&uNZOgFsLGmJ9uZ+u?QfsSD9O(z@M05*T# zHh*|<NFU1XFChtccdIQse~Jhn6&N6+-&%aa!p(V|!td$l}HrIu zx)aGsBC0BJb6MN1At^~q^RuygT{(2!KtoFilTqE)4{>w4SZonQORp}tu04cK@R@9_cso;UvxDjZmzCWSF{VDBrg-M)>MeAc`Ehe zk(i4GK-@Eh$O=u-QCi&|!cpoqQFy-Z{1aW*2Hi}i^degN1pRjC!{R74y}cFwtOa%u zDX)9F3>8I>;MQ;$U;KF95gV^Gzy>II+W-bf#!#5E2%qh#L1xChc-xtIQSM^w+YpfJ zkAL_TV-wLCo!f-Cxj2tKcEFFCf5DZTLs;(5LgTLWpxQqB+5ciCe3}=>yRPEwm4Vr6 z=e6MG)Wg}K!V?j^bEE?ULt#8woQHFJG)kCd$HZnGe^VbKlF;IdPVzX(Y&}?5_DnCtocP|eAblGf>#ELQg^~nQut@0RB z_bP0{PIj03?qdIUKO-8`v1k1XR6hQw%u3_2-<-_sbm3{vsq2_pBh0HT0ZVJ9b4UE?h(IVjluxQ?_5~RpxUUL&T*F zOl58^O{q`wAenNjh~((hloCLu#mk--r=rhfXt79zoZ51gdgU_VE;tw5*-fpp0E84M z4P%IW&i*GLzX<`5kfO|4x};icmU^*tJRO90c<9%xo_x)Ib7f=N14qu literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/32/test_connection.png b/src/main/resources/com/rapidminer/resources/icons/32/test_connection.png new file mode 100644 index 0000000000000000000000000000000000000000..1f659a2d6530995cea50cfbd0cd4d987dddb46dd GIT binary patch literal 1024 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 bNCN*4Y!G8dn)Bfv00000NkvXXu0mjf7BKd_ literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/48/@2x/gearwheel_right.png b/src/main/resources/com/rapidminer/resources/icons/48/@2x/gearwheel_right.png new file mode 100644 index 0000000000000000000000000000000000000000..e7facee108626ca91682d8b15356dce6140ac727 GIT binary patch literal 6190 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/48/@2x/question_blue.png b/src/main/resources/com/rapidminer/resources/icons/48/@2x/question_blue.png new file mode 100644 index 0000000000000000000000000000000000000000..bd5adda0f0845f6ed5c385973a8585a166016742 GIT binary patch literal 3948 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/48/gearwheel_right.png b/src/main/resources/com/rapidminer/resources/icons/48/gearwheel_right.png new file mode 100644 index 0000000000000000000000000000000000000000..8c194a0140f673dcd65c1e43e86d333eecc44eb1 GIT binary patch literal 2929 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 b*ZzM2opF>qz))o?00000NkvXXu0mjftbeo4 literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/48/injection2.png b/src/main/resources/com/rapidminer/resources/icons/48/injection2.png new file mode 100644 index 0000000000000000000000000000000000000000..6b2b5f103f5c9d992ca608a86ea8bd162f8f1f51 GIT binary patch literal 2141 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/48/sources.png b/src/main/resources/com/rapidminer/resources/icons/48/sources.png new file mode 100644 index 0000000000000000000000000000000000000000..d776beb4708f35690843bb6f6a0a42691bc186ce GIT binary patch literal 2309 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 xj)d?~MNsJ%;d;zY*gia93D4L`RbDwR`{o literal 0 HcmV?d00001 diff --git a/src/main/resources/com/rapidminer/resources/icons/64/gearwheel_left.png b/src/main/resources/com/rapidminer/resources/icons/64/gearwheel_left.png new file mode 100644 index 0000000000000000000000000000000000000000..6be8b471974d53d395cf16c64eb342835f492707 GIT binary patch literal 3975 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 diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/area.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/area.png deleted file mode 100644 index 3ab685c7b9f41fac6a03f46a405db02536193a67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5587 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 diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/areaspline.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/areaspline.png deleted file mode 100644 index 505dc903320b2b8a1e30d93ca80569400086a687..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5737 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 diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/bar.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/bar.png deleted file mode 100644 index 7c177e02c5d096f09032d4d936c9080b10fe967b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3235 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 diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/bellcurve.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/bellcurve.png deleted file mode 100644 index ba7db1650a5be1ab82e56c7c4f588223d71d4bc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5619 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 YZm$>$*iVQ)RB-`?H>xsaQl>%w2l)(Fp#T5? diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/boxplot.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/boxplot.png deleted file mode 100644 index 1b578fb02da11e5027447eec329c10a7e1a3b9ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4514 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 diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/column.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/column.png deleted file mode 100644 index a277547106f033e6408f7f184f69e7937e93b8e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3035 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 diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/funnel.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/funnel.png deleted file mode 100644 index 9908b8445f5c6b6f34966f39b759f6efba8c9823..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5085 zcmb7IXH=8Tx_uJ@BtYmb6bZdc6_jS^y@P;K0tiuhmm&}#^e)wpVgRWMN|O$e4kEoP z0#YO(AR>aOpEuvS_naU1&spckyJo#JYwvkx=6Uv>HOXcsx^OBEDgXfB`g&Rxq?Yi{ z2c-Z2kl;J}Y|;-X&_Y)OXqe<80svEizLvUGDCp23B8y8K@#JWwXU$lzeNC0XC}F6l zVUcf31*VF%0gtW0pnCG}9nJ6%fevMn(ip<-6|6=s>Zyx_MgS-7ro3Q$7FdfO!h=Lw z2ncjPg={nEX^h52Ja^)uF$%$iV5+*Rs~_~%um1J8^hMvu@zT1{l#yws^9CJ5k_!0g z>;QMAMn{vxXh@akS-a0|yOmXQyCct{EOo=)acpZG=CNpQ!^?eT4tmC+5mqj^?P8pS zc!^=Vs9ooS`onh}>3eJn(Dc753+=pef5fyS#wn^pQ{dD|ovHucat zeY6v<9JV&;Z(xviZfi)&DL?XxeqtGBOlHFJD}Mo(Bzih497}PM9f!Sw|`pl3F0*YGV+i0U*`BIk=Pt$+5bBY5zDf>4|=TFs<6(eoOK zG|P!vsV}VthU50c4mYc`Lc;c2$}sP}=8Zm%8>9`tIgS#Z|EX%Ch(3%8b0^eUnU)C( zyAX%+kAXs0N9I#XROwmyfDKcVc=2qneDluQ4EJ~0>;pgT3qh$%und9uPZIM33j@{& znU6NZQ#)Q8K%wQ@N-y;$7q5IR)0k1}6Kn??F<~XI!F$ie>Fg=hMMz06=d=_Hp24Rk@NSR z4|xJn+<%`*iLzYoj}2(;$bytg?uvM1UuPmQ9V+ner~^&33ldy9&`^Cmry z*2d>2EtW%G7K33;I|eS51IoT-Zk#MAip_+qe&@#KFe2Q3d;jPQpWzh2`dw)b_g|NO z$Q}P#>~~ss>(XgcFJ^KgG}~1+h8@jf?D49pGL0t!cVO+i0qkN@>B6pk)TPxK@;c|| zxemBP((2D$CsMskdwt8+;so9S|vu!PYz9|HKas=2M2ck9pkK3dc+os<*gr{P^^wFX|y-?(*!QE&t7qKK&w8YbRXTR;*aYTJ1xu8pRWJ z1<$Hi?@cQyFxrVzGPn^BE?Ns3hiir$@*6OxbI*HKxjaW36e3~+{I zY39>a3o9CbKR|7b<}6t_Jo7o{%#;QfItP4AIiDNGOTyI4ipy?Y=RK!fhMp^ zXZ#-{i}M*5v!Zb*npQYaC_N#H+~;|sAWD?E+K-*o{(**t`1FBHO|w4V_?`7us^z%dUpEyRPc(mHCG|l-x!x$ zC+`viN)LQ&!`==ZWi|-x;Ydm4nrLWRF@0_ik=D%exsG2-Xun@5ysU805WoZw6`nQZ zvBLzqZe4PGm@)(=$K~FhQ?hR;ATsM22HsCG=O6y6pK`+A=FD|S{*w* z_d4;t)WS%)u5YqGTXfA5gPu_$`btJHn-FP?h)mz`R_?9f+Sg|#T%r*9;D(g9@0o~` zv#Kg^9a5$67Qz*0Eg4U-?hEmp*$4G!c*FaUr_`FBaY~hjJx%vtom_J4m37-xkp18+ z;Mi#lbKD8eiOXc_=|5<0p+RTBUwzs=RbpSc#6cg=I*NgK;@B?aqO-|)xQcQG?0a~` z6^M{&t?~ugMZGtMnQy%r{;AqajDn(l0dx4xn4^bE3_>MX{|ekZH}CGYz1169k2fWv z<85Y8cCRVwKwGN~m2h_^*>THtM@43^nnL@4kL^;h2JsqSBwZ$8P#H;OAb>mzg179j z&gYyo-;@VaybFjv*|N32tI3ZAQS~G$2T|lRYF@;U8L!uVW1R$fD{o%EOO`fMqoV=Mjk6Eh30T-oT0=k&Yo!0r4PeSd~ z#CWmfWcB-3?bMSOBZNA%RwaYoAVC!K#77>Bd*?+O4j{O+0(kxRAOlWP-mDiu87iw zH$^^q=%B^QFb=~a3r49jbftzHtmgyQE#pT%VOQBh5TGj>dx zY!je+N!UC<=`ur&AN(GQ2K#9yQ3dG$OeoCv=?6X#X;3?_7P%5X7KWFL*i%*n7Xt6; z%fTfK1jzJb)o&%7AS1vBY92F5g6>URxtvS~gaAcc4GKeo2D#Hkej;&UIW?^CawMoy zb7F5GiHiW|6`Mwopd~As>m4MnjRbm@y)-}wJNdcn_`eAQ8tl{iwFG0d+8TVZ&}8nj zh>-v0APE+#OuQCM`&$#oR#plcO&0%PYAj&65x2vW~QDv)LnHVr1g;?3+Qoeywf_lhLD`mg^}4Dy@N|pYMEN;qGdJpktm=wW;YE z2h{B0&753$fwi5T+R0fCgt@3Oz-l&EJj+D7FG%uVpRc_M_S=pYv}RY*y(KWtgeA}a zY=0)r+v0F5tH#C&0=@PQ340ox!=DYLl?FNAlN9`B6rOl5LA?laB3h24l6B3!N z59~4*??-tuUTSv5wO=uX;Mb!~JDY@$`_Y~Su5?TbZ?vbseX;Y-ZS`f4t}Li9uvUYJq2P_ouV_!B1b?yL z>ojs+AamB{T!Em0yHu6WHfRYSUlLHt9t_xqDLG>Qe(JzCzF8OntH)-*rokvd&BWZo zq6VJ67_ks#ysZB+DhyPBA8pw0(dPA6lS?MV1NNsS>h5$stof>MGFV}R;`pO!`|C`4 zVO%~jF^1ZtqX`K)H;oVWX2iyjVPY+jup?^_^J;E95GLMpoZHdVvNZS18vGKCgt@;y zKjf)rV?SCV1IalDWi5V867y{}1SqVkUS8_8-wQWjxp&Wo20VS-7Yk2du8=IuH(Q6% z?v@AETWw-BeL6fBc|s7zOeKOZ<;S6)Z@k*6?0;~F()wqOH<;dnOT6K*Hz?HWvs0bA z*3T#gWQDJ(LFBrxB;xDskQfPMigyPFJ-A`FsBrZRg`NFsH?*kwHygQcs|i4{fx7cI zYx|W8Yi5EL&ptn3Ur+S)w6k-P<$@hDVPDHnXWyh8&f3uSzwRyclBGSq=b8-f62dkH zmjcfQXbN!Zz2t4W{=K}gbS?b2WorH1YE0VI=YRkZhmUrDvtA}X&&@^qT$CIX+8Fs#RnaNM$D|UT2hgO(e9tdRa^4`^m`vbp z3ZE4iNu3wAuo(~p$T0W{x2x~HZG{;{=2>V!*V!dSCMQ%mpTGKb8~zxec?GN8R@^LS z|HPBGo?tIYP~Vfe&hD@!MjyisFCr=Qz5Z_y8W)51_aWGf~Wt< zpqKD3x_1FVL(jHZ9Vnpp8am`y(=%^LfWVO4-aof6iGBa;4h!y zy6DCZJ5{w1ZPg?<7c-2((zbJU;q2Y^eK+irwp`(tzcFup9WR#<^E$ z^rfqq^p!5CFiY<`o3#&p6b8jWwwjmvfhU3&va1r@9CGbCw~<8@{s|-ialH z3ZUT}BPSTwoilcLPS&>8 znLDBe+F*99vJ00EN_<7sxXt{_$V};LL;>RM8~RJoTTWTvWZdD>Lj~>rqyWK{G<79w7*$n zNlH%7Zl2umk1LFGT}nboTF)z8&gk%uZ{*$hb_YtrYM`|QJNZb?A|ANcMi225!NG^$ zOsyZlmvKai6E-%;7aDrDDj5@95-peW>;85m7WQ9vn4POH52teqkC4-(HE%L3Ukl!! zU+d+xQ3UDe=)96wkzhxA(c8B9JY*elo|C65QSe@!TP9C|pRw1#W44!FHg8L@5vZw;VHm>C=FqU1m{Eg_oVkaKwsNLt3d-D`#*OT`lSE> diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/heatmap.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/heatmap.png deleted file mode 100644 index 746d1529262fb1900aec258e0448f00063534c39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3132 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 T=p6+94gkg%%neEjq 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 diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/line.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/line.png deleted file mode 100644 index 5f8a9ce60d04888ae589792231617e49e3c0018f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7227 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 LUFBLu+o=BnbUkR@ diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/packedbubble.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/packedbubble.png deleted file mode 100644 index b70d87acf13bd65c2396016bda55946e16de837b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9000 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

      FS1s`vk_htX*R@9?n<1BccBZO2&oexbmx~0aVjg z`9MxX+qR)L|HmH5V$G1WqGQ6${zi0$FKdf-BbzlQ)G(Q~RasYq?N44KZ2|R(nLY0z zui+4$brl9|%z(79T6Yy&ZTtc;nlO3v^tQv*IL`Xcye$&^1Egy1`{f_Vh{5kTfPGr-_AiqmtD%*5qoS*e_;_kIm9NK?BO-S* z=TMYETLW^uF28+i@$0#JIbZ?B(^>JJLiIv{V9;g=6g&57naQQrfN@5ZPW@Nyj~(tkSev{dxC*DZlU`_q7dcIT-rpJq(qaoabN26p$UrLcO!T%o3N7b$ z#XY@sHa2;qu`R8<07S#xt%!&Q60olT|8qdkiT&s??)m=Fbcw3b?rXCa_moEm8C9ey z8Gxt<34R@w|Lk}k93_&rqKq_>w>OTix5_zkd5KcUt!?VAa(tOUFm5gmVvwl4Qr5lo@*lwv30wf}{m(eoHPGHdxp9%R zV$B00yUM8kxrwVBGr~%PIWtFx9#iO~8pQ15c|vol|EAhclA}^UqpOue-v@OZow93c zNU3}os65I80 zOX>){U>mxUH5C}VzPjWL^D?at7X#nhk%gM!`ERy0FK8{7smTkq)3r&R@4UPV7{N~% z+aJ0!k&kjxnvPJl&s?DS4vq{PP+J*Mtij*EahqqRI=qM_<;gFQ zYbrp`>~H&G*4S|Sh8Fg$-KK$K&BpqWJpt zxU(!q$hE2WZLY>xnlHsg)6e64{afw#;ljj_P_HH>Dd0FD$EE6%eaC@Q{xm7}BGBc$ zx_z?pTYzlD+7y?w9*OC#u0j6$r_hhYRA|6 zstjF|>THc6bEIn`Sg`rOHR^a8f; z(m%gvG3v2HF~0$@=)l+Y#TvI1H&HUCv!mf*{0!YIKrrER5~KQ{cj6us{X?Or(W&d8 z%@Y3pEwI1Df5a}r+KI*S#GOGr&x(v^wxV>ZKF%JnEo+SN3GMN4f@6Wg|NJOHK$&T2 zMHwQY$;WXrsD?ocM+|<1il}}RX!9YpRsF26{WGiTK@Kw^onh;Atv5c^Pq85N?}#tw z_=obW#K79XCN3h@Bqq<)@T!f+XF-WHO)-fZx@z5kP3DPJkk6rWT4b1{{@Y3v-UoB( zIq}*#&oOn48pjX&3etp>p?R!|CoZTu!PN?=s;6cbFFxSHOj&L(OnhP-(F#AHk7LBa zryv)deb#E@^qYNmM9@iZ2G4bSr@$w4j0HVu?(Ld{lt?C>J9Rlt_R1E1XBAq}>K*TM zEErWgODEH!2VB(x#VtJk#pZZ8Zq3h+!%<{khb!bF5-zBQ<>t<2g$=KQvqG5iO4kxx z?T-afX%?*+oIi% zpWtA<{jZm?PU?K3ilNV|wKE!^i(x|3bftaUM~O1QR*+jn7KV!^)%8inJgd!l1q&}I zr@Lm1GmzTqJHJQ$Q7Y>{vi{S)3@@1KYl$kRSF61^L?abox<0>8JIG5a66J?7Ykk5k zkXcI;Nc_AYDWe=A@>92vW13FD_j^54BjlMpglr5T7&GQR5V>+ipQ48)k9eQgkC@r1 zrmfXj*K`_z1U==N)lViR_h0a$oOD}%L>xF}v zHb#19cYAW7!v@3Ly<|p((2mA13d?E z6jZti&mjDV)+0OVBerL~X3ukGT31`!v+$;W0ax2xhHfq<>N-!vqz~-veu~pLi{8Ug zNZ$zvXbwN$lQ)DUalbSd{(apW!3R!_NQIu5ZQnPf7j!bthy!-k>7@@I9(me6`OtVa z*4H59EoCuJs^wN&Ov-U`rWYgS2jIj^GF)W^CEf9XI{?SlATygVG4VegN(#QU(MJ>V z+H>6o-kWRaT1IUBURj~a?LVShDormK~0P_gzcmTaNe%J>>pTzNOc-fxua|y+HtHrhI*P(J`>^-@_gOG z1&IUXZZj_yr~@en=mh$B);!ea=tHkcDQ_OB#5}yHVV4@|CO$`j91o$XpoK9#&?`y5u}T z@LtF;@oI(K*ch5Apj`kp=rmk=hKTDDhg|FNC$Zax*lzu$G^fu@&_$;oPm1j?Uta)a Mc{RBz8H?cm0LL8hB>(^b diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/parallel_coordinates.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/parallel_coordinates.png deleted file mode 100644 index 51ae8e6bade3311432bc9daa6daf1804878dede3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14243 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 p{D{y_Hw?Szgu?@a#G5Ywc@5B{|nWHCWrt4 diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/pareto.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/pareto.png deleted file mode 100644 index 1279f2a9b7ef0737374cbae2771f84e9d7960d9c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7934 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 UDH2xy?~4*pRn%0dm9q@{4;-M9RR910 diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/rangeareaspline.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/rangeareaspline.png deleted file mode 100644 index 28dad85aa68749372ec7275ffa3c2ff742498dea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8500 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 Thy*@=g8)<&wG?XPEW`c-$+pOb diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/rangeareastep.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/rangeareastep.png deleted file mode 100644 index 03482fb9be6783b631757ab69b58e579e4943a51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3283 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 F`~&9;nfL$z diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/rangecolumn.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/rangecolumn.png deleted file mode 100644 index 527eca632d2616283c0f8c758a616b5cb2cf3175..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2999 zcmd5;X*kqt8~^|RGifj~gDlg;Xiy}@ktEB^FqUHp$rhRH$=I`Gn^B4;YdX~ESZlIP z_GHPFgvwU7P>GaeRH!sE-g(dUemZYo-s^mMKRnm-yYKt?J@>Qxp8I-IZEeiNL@*)% z0Ek(bo7f9_&c7Q%2mnCk3AZeP0fyU~kpOld_5%QjhFX{q9il<*b?ZymPPy|eztKnu z20_a$6SetVqEM;%E{WW!eBi=Yw3Mn_7-`aOhc}UeckV&Jzvsu9PG`bI?n~pv`97@XzF@QG+2yw@0u(Gq`XY|T8}&~FNA)M(?lM9U3bQ$N z!SRMy`M0rwO*OAW5@tR;Nptd-&N+1a(CJvs0_oPX(UN0}2uIA^YTv=p^)wkf_u*O1 z-6pe}nkQ!6>-1S#^q{bqA};qaoWY1!al6#J{%WC68$YzBPg}`P*6i zcz89lpzY%EFFZ?@(BW;C+E$2H^gHlo1`sS9QJS=K3}4I6Ui$VUecwx8y$P4og)_%& z%3G*c2zSPOV?V>Iv~QKWu{!^RH>VyYQX9Dzwhi5?s;sAKrL$Br#bhH-Sv0?NGV}Yy z{j`O)>13LAHDK~nLnH<7VV(ks5*~hXy}s}yH|4oVyn*lMLF!UV<`u}${KX?@huCKJ zVwm`*z#g0T`){0*oS4v%D%NERWxr0&4(3c~c;oN%V#zC)dRu>BD}?5^cr6p|@$W^? ze|v82soXBwdu>+HN3z$T-#S6&_j`2Y@&2XO!xt|w<>|U2sa~(G^IB6+Qk9*d~nPS?WGTUWRqLrG%%etnH zH`V56WSS#BJgcayd*8{BX}7x#P8N@kJGyU2TuhrffX?F*U*9j-l5*T-(xJz&?Q(r` z_y7{rb3}uvLN91Ls|!R7XwuW4&j9ebJVMupPymXsDx6u-1wcKOfzUHp8n9h*PPZ+W zRZwAgqNT<6`B?7vgYeT99)G_LLDaG|ige~;sSsg9zOa<2jb%av^YyVAoA7@c- ztJr5PM`I5>l4+AOWd+J@23?bu z8<60y@uh+bOXON|enNX6-as#JZoO z*6vXL^Q9TZa|}JVdo}UT!$&l;@`6q513(EzRd1{V~8Vd@Kj z-nysia5U+(rvTI)GU7`F_F%4p@XUWYF@+C`u>#BD%veV*)1_x<1PZldhd_UXWfTtO zJW-9HSgH-^!x8Wi9d&3dTSQwNgy;=oA7?{p)Y_qUZDB!Ss}hn38i=`8>IN+|vCTQqy%5vCwf*Iws@&}`3#Ab`(Rti2F+MGfe*+~Qpwq>E= zsQZa9%h~}tjF{%)^KGiteOKueNWw1$=naRxh%7xEG_HRFisHV6Pr(D}#Ck8gJuQ|2r z=;$oUXLR*fP>qfh#}%8zxU}g--t*LC;xfhPSHYp=ewX3FV3`(s)9Az3Zz;G<_QfOK zk|+scD~$5w6khm|@X+HcIc3J8Umn$vQGDYTs7l1M*DmLsPK+N;n& zd&i9N>%tVk?;PA*Fv{G;ARtQM&PPk&d>~nz&(~Wo|NqFS-o4tbMF|a4PnHe*jQQ{1 zw|xn)TPRf*X?TK8c?2l5u6H)`bmq<6j_n;`j2^wja-XSANJNDgPA`gEMO@ttA~P#$ zSA56-%=v_;JsZA{z<&HJcPogbB&S;3mEZt()hs<6;l(kSp>kIhvKi%TNe@wh+!hiX zY`Lyhw%T9s1t`pOpUd9PXJYyDOIIok#}gM{uS=@+wHSKtPra{fE9v^QHyse_wz02Me$oI?eXFUQbn}1?`Id^B&i(P3T#_safr^=Z~ zzgm;u0hoyz(x(T-5mdh(2~`k@D%+Q%Yz)A)Pec{YU+>NL9_4gRr^o>Ua-S$~$EBqzAv3{%T3lX@1^4 z|9OeT2983;VPKW~`H{VqYQ7S1eTgZFZoC!gh8X}WWyx)gRic6V50o6v2-b1tnq7^V zK+%7Nao7Clp2l~YwN{;nb5kiaf0CGJkzfBjdX}{_EQM z6SiiUi_xLZ4ATOG>68|0&ix2{*Ff#ca;?$Ux((pt|D U%AFQc1V3@Wf^1{LCQ*|91r1&$EdT%j diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/rangeerrorbar.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/rangeerrorbar.png deleted file mode 100644 index d829076a6a811db35a04e75df78e216c9e3b022a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3520 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 Nl$g9|rLaNZ{{yY7OjrN_ diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/scatter.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/scatter.png deleted file mode 100644 index 95e3f16ec30a1f197e419ccfe42323d49ecdbf13..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6678 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 NKvhvop$1_U_FoI>ZfXDk diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/scatter3d.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/scatter3d.png deleted file mode 100644 index afea0b27c5ce49bb9c20a12c96cc99c8ea8e6552..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9727 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 diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/treemap.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/treemap.png deleted file mode 100644 index 2892c752a38324c37c833741a2e7e5ec8c838579..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3259 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 diff --git a/src/main/resources/com/rapidminer/resources/icons/96/@2x/wordcloud.png b/src/main/resources/com/rapidminer/resources/icons/96/@2x/wordcloud.png deleted file mode 100644 index 8618dad1d17758e11cf65dd23d9d3143f74517d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7020 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 diff --git a/src/main/resources/com/rapidminer/resources/ioobjects.xml b/src/main/resources/com/rapidminer/resources/ioobjects.xml index b9472b3b6..b0dbcbde1 100644 --- a/src/main/resources/com/rapidminer/resources/ioobjects.xml +++ b/src/main/resources/com/rapidminer/resources/ioobjects.xml @@ -34,7 +34,7 @@ com.rapidminer.extension.html5charts.gui.renderer.ExampleSetVisualizationRenderer com.rapidminer.gui.renderer.data.ExampleSetPlotRenderer - + com.rapidminer.gui.new_plotter.integration.ExpertDataTableRenderer com.rapidminer.gui.renderer.AnnotationsRenderer @@ -47,10 +47,23 @@ reportable="true" icon="sort_descending.png"> com.rapidminer.gui.renderer.weights.AttributeWeightsTableRenderer + com.rapidminer.extension.html5charts.gui.renderer.AttributeWeightsVisualizationRenderer + com.rapidminer.gui.renderer.weights.AttributeWeightsPlotRenderer com.rapidminer.gui.renderer.AnnotationsRenderer - + + + + + + com.rapidminer.connection.gui.ConnectionInformationRenderer + com.rapidminer.gui.renderer.AnnotationsRenderer + @@ -216,7 +229,10 @@ icon="lightbulb_off.png"> com.rapidminer.gui.renderer.DefaultTextRenderer com.rapidminer.gui.renderer.models.KernelModelWeightsRenderer + com.rapidminer.extension.html5charts.gui.renderer.KernelModelWeightsVisualizationRenderer com.rapidminer.gui.renderer.models.KernelModelSupportVectorRenderer + com.rapidminer.extension.html5charts.gui.renderer.KernelModelVisualizationRenderer + com.rapidminer.gui.renderer.models.KernelModelPlotRenderer com.rapidminer.gui.renderer.AnnotationsRenderer @@ -702,6 +718,8 @@ icon="chart_bubble.png"> com.rapidminer.gui.renderer.math.NumericalMatrixTableRenderer com.rapidminer.gui.renderer.math.NumericalMatrixPairwiseRenderer + com.rapidminer.extension.html5charts.gui.renderer.NumericalMatrixVisualizationRenderer + com.rapidminer.gui.renderer.math.NumericalMatrixPlotRenderer com.rapidminer.gui.renderer.AnnotationsRenderer @@ -713,10 +731,11 @@ icon="chart_bubble.png"> com.rapidminer.gui.renderer.math.NumericalMatrixTableRenderer com.rapidminer.gui.renderer.math.NumericalMatrixPairwiseRenderer + com.rapidminer.extension.html5charts.gui.renderer.NumericalMatrixVisualizationRenderer + com.rapidminer.gui.renderer.math.NumericalMatrixPlotRenderer com.rapidminer.gui.renderer.math.RainflowMatrixTableRenderer - com.rapidminer.gui.renderer.math.RainflowMatrixPlotRenderer - com.rapidminer.gui.renderer.AnnotationsRenderer + com.rapidminer.gui.renderer.AnnotationsRenderer @@ -797,9 +816,7 @@ com.rapidminer.gui.renderer.DefaultTextRenderer com.rapidminer.gui.renderer.AnnotationsRenderer - - - +