+
@@ -663,7 +658,7 @@ angular.module('mc.core.ui.states.defaultStates', ['ui.router', 'mc.util.ui'])
{{natural($stateParams.status)}} Models
-
+
@@ -997,6 +992,9 @@ angular.module('mc.core.ui.states.defaultStates', ['ui.router', 'mc.util.ui'])
'''
])
+.config([ '$modalProvider', ($modalProvider) ->
+ $modalProvider.options.backdrop = 'static'
+])
# debug states
#.run(['$rootScope', '$log', ($rootScope, $log) ->
# $rootScope.$on '$stateChangeSuccess', (event, toState, toParams, fromState, fromParams) ->
diff --git a/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/actions.coffee b/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/actions.coffee
index d63198fb48..9114dc552a 100644
--- a/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/actions.coffee
+++ b/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/actions.coffee
@@ -116,8 +116,7 @@ angular.module('mc.util.ui.actions', []).provider 'actions', ->
action.children = ret
action.sortChildren()
-
- $scope.$watch watchExpression, updateChildActions
+ action.watches = watchExpression
updateChildActions($scope.$eval(watchExpression))
diff --git a/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/bs/menuItemDropdown.coffee b/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/bs/menuItemDropdown.coffee
index a8b57fea8a..3e246aea5f 100644
--- a/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/bs/menuItemDropdown.coffee
+++ b/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/bs/menuItemDropdown.coffee
@@ -1,9 +1,9 @@
angular.module('mc.util.ui.bs.menuItemDropdown', ['mc.util.ui.menuItemDropdown', 'mc.util.ui.menuItemSingle']).run [ '$templateCache', ($templateCache) ->
$templateCache.put 'modelcatalogue/util/ui/menuItemDropdown.html', '''
- {{action.label}}
+ {{::action.label}}
'''
diff --git a/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/bs/menuItemSingle.coffee b/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/bs/menuItemSingle.coffee
index 83ce335c2d..9040f9764e 100644
--- a/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/bs/menuItemSingle.coffee
+++ b/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/bs/menuItemSingle.coffee
@@ -1,5 +1,5 @@
angular.module('mc.util.ui.bs.menuItemSingle', ['mc.util.ui.menuItemSingle']).run [ '$templateCache', ($templateCache) ->
$templateCache.put 'modelcatalogue/util/ui/menuItemSingle.html', '''
-
+
'''
]
\ No newline at end of file
diff --git a/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/contextualActions.coffee b/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/contextualActions.coffee
index 356c00bec0..3ac03928e1 100644
--- a/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/contextualActions.coffee
+++ b/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/contextualActions.coffee
@@ -19,7 +19,30 @@ angular.module('mc.util.ui.contextualActions', ['mc.util.ui.bs.actionButtonSingl
scopes = []
+ $scope.$$actionWatcherToBeRemoved = $scope.$$actionWatcherToBeRemoved ? []
+
+ removeWatchers = ($scope) ->
+ fn() for fn in $scope.$$actionWatcherToBeRemoved
+ $scope.$$actionWatcherToBeRemoved = []
+
+
+ collectWatchers = (action, watches = []) ->
+ if angular.isArray(action.watches)
+ for w in action.watches
+ if w and watches.indexOf(w) == -1
+ watches.push w
+ else if action.watches
+ if watches.indexOf(action.watches) == -1
+ watches.push action.watches
+
+ if angular.isArray(action.children) and action.children.length != 0
+ for child in action.children
+ collectWatchers(child, watches)
+
+ watches
+
updateActions = ->
+ removeWatchers($scope)
hasActions = false
scope.$destroy() for scope in scopes
scopes = []
@@ -37,19 +60,23 @@ angular.module('mc.util.ui.contextualActions', ['mc.util.ui.bs.actionButtonSingl
scopes.push newScope
if angular.isArray(action.watches)
- watches = action.watches
+ watches = action.watches
else if action.watches
watches.push action.watches
+ watches = collectWatchers(action)
+
$element.append($compile(getTemplate(action))(newScope))
if watches.length > 0
- removeWatchers = actionsScope.$watchGroup watches, (newValue, oldValue) ->
+ $scope.$$actionWatcherToBeRemoved.push(actionsScope.$watchGroup(watches, (newValue, oldValue) ->
if angular.equals(newValue, oldValue)
return
updateActions()
+ ))
newScope.$on '$destroy', ->
- removeWatchers()
+ removeWatchers($scope)
+
if not hasActions and $scope.noActions
$element.append("""
No Actions""")
diff --git a/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/contextualMenu.coffee b/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/contextualMenu.coffee
index 8d7c0849de..8dc7ce2dbf 100644
--- a/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/contextualMenu.coffee
+++ b/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/contextualMenu.coffee
@@ -12,14 +12,68 @@ angular.module('mc.util.ui.contextualMenu', ['mc.util.ui.bs.menuItemDropdown','m
getTemplate = (action) ->
$templateCache.get(if action.children?.length or action.abstract then 'modelcatalogue/util/ui/menuItemDropdown.html' else 'modelcatalogue/util/ui/menuItemSingle.html')
+ scopes = []
+
+ $scope.$$actionWatcherToBeRemoved = $scope.$$actionWatcherToBeRemoved ? []
+
+ removeWatchers = ($scope) ->
+ fn() for fn in $scope.$$actionWatcherToBeRemoved
+ $scope.$$actionWatcherToBeRemoved = []
+
+
+ collectWatchers = (action, watches = []) ->
+ if angular.isArray(action.watches)
+ for w in action.watches
+ if w and watches.indexOf(w) == -1
+ watches.push w
+ else if action.watches
+ if watches.indexOf(action.watches) == -1
+ watches.push action.watches
+
+ if angular.isArray(action.children) and action.children.length != 0
+ for child in action.children
+ collectWatchers(child, watches)
+
+ watches
updateActions = ->
+ removeWatchers($scope)
+ hasActions = false
+ scope.$destroy() for scope in scopes
+ scopes = []
+
$element.empty()
for action in actions.getActions($scope.scope ? $scope.$parent, $scope.role ? actions.ROLE_NAVIGATION)
+ if action.active and action.disabled
+ action.$$class = 'active disabled'
+ else if action.active
+ action.$$class = 'active'
+ else if action.disabled
+ action.$$class = 'disabled'
+
newScope = $scope.$new()
newScope.action = action
+
+ scopes.push newScope
+
+ if angular.isArray(action.watches)
+ watches = action.watches
+ else if action.watches
+ watches.push action.watches
+
+ watches = collectWatchers(action)
+
$element.append($compile(getTemplate(action))(newScope))
+ if watches.length > 0
+ $scope.$$actionWatcherToBeRemoved.push(actionsScope.$watchGroup(watches, (newValue, oldValue) ->
+ if angular.equals(newValue, oldValue)
+ return
+ updateActions()
+ ))
+ newScope.$on '$destroy', ->
+ removeWatchers($scope)
+
updateActions()
diff --git a/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/resizable.js b/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/resizable.js
index 3fd85db5f9..7e4a2af728 100644
--- a/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/resizable.js
+++ b/ModelCatalogueCorePlugin/grails-app/assets/javascripts/modelcatalogue/util/ui/resizable.js
@@ -1,75 +1,125 @@
-(function(window, angular) {
-'use strict';
-/**
- * Extremly simplified https://github.com/angular-ui/ui-sortable
- */
-angular.module('mc.util.ui.resizable', [])
- .value('resizableConfig',{})
- .directive('resizable', [
- 'resizableConfig', '$timeout', '$log', '$window',
- function(resizableConfig, $timeout, $log, $window) {
- return {
- scope: {
- resizable: '='
- },
- link: function(scope, element) {
- var opts = {};
-
- angular.extend(opts, resizableConfig, scope.resizable);
-
- if (!angular.element.fn || !angular.element.fn.jquery) {
- $log.error('resizable: jQuery should be included before AngularJS!');
- return;
- }
-
- // Create resizable
- element.resizable(opts);
-
-
-
- if (opts.mirror) {
- element.on('resizestart', function(){
- // stores the width with padding as when setting back the padding is stripped out (setting with
- // outerWidth does not help at all)
- // this is probably bug in jQuery and there should be a mechanism to check if it isn't fix yet
- jQuery(opts.mirror).data('resizestartwidth', jQuery(opts.mirror).outerWidth())
- });
- element.on('resize', function(event, ui){
- var delta, newWidth;
- delta = ui.originalSize.width - ui.size.width;
- if (!delta) {
- event.preventDefault();
- event.stopPropagation();
- return false;
- }
- newWidth = jQuery(opts.mirror).data('resizestartwidth') + delta - 1;
-
- if (newWidth < (opts.mirrorMinWidth ? opts.mirrorMinWidth : 200)) {
- event.preventDefault();
- event.stopPropagation();
- return false;
- }
-
- jQuery(opts.mirror).width(newWidth);
- event.stopPropagation();
- });
- jQuery($window).on('resize', function(){
- var windowWidth = jQuery($window).innerWidth(), elementWidth = element.outerWidth(),
- newWidth = windowWidth - elementWidth - (opts.windowWidthCorrection ? opts.windowWidthCorrection : 1);
-
- if (newWidth < (opts.mirrorMinWidth ? opts.mirrorMinWidth : 200)) {
- event.preventDefault();
- event.stopPropagation();
- $window.resizeTo((opts.mirrorMinWidth ? opts.mirrorMinWidth : 200), $window.height());
- return false;
- }
-
- jQuery(opts.mirror).width(newWidth);
- })
- }
- }
- };
- }
- ]);
+(function (window, angular) {
+ 'use strict';
+
+ /**
+ * Extremly simplified https://github.com/angular-ui/ui-sortable
+ */
+ angular.module('mc.util.ui.resizable', [])
+ .value('resizableConfig', {})
+ .directive('resizable', [
+ 'resizableConfig', '$timeout', '$log', '$window', '$rootScope',
+ function (resizableConfig, $timeout, $log, $window, $rootScope) {
+ return {
+ scope: {
+ resizable: '='
+ },
+ link: function (scope, element) {
+ var opts = {}, setOption, recalculateWidths, getAbsoluteWidth, breakIfNeeded;
+
+ getAbsoluteWidth = function (widthInPercents, parentElement) {
+ return parentElement.innerWidth() * widthInPercents / 100
+ };
+
+ setOption = function (opts, option, value) {
+ // always set the value in opts for better accessibility
+ opts[option] = value;
+ // in case the resizable is already initialized, update the option directly
+ if (element.resizable('instance')) {
+ element.resizable('option', option, value);
+ }
+ };
+
+ recalculateWidths = function (opts) {
+ if (opts.minWidthPct) {
+ setOption(opts, 'minWidth', getAbsoluteWidth(opts.minWidthPct, element.parent()));
+ }
+
+ if (opts.maxWidthPct) {
+ setOption(opts, 'maxWidth', getAbsoluteWidth(opts.maxWidthPct, element.parent()));
+ }
+ };
+
+ breakIfNeeded = function(opts) {
+ var windowWidth = jQuery($window).width();
+
+ if (!opts.breakWidth || !opts.mirror) {
+ return false;
+ }
+ if (windowWidth > opts.breakWidth) {
+ return false;
+ }
+
+ jQuery(opts.mirror).width(windowWidth - (opts.windowWidthCorrection ? opts.windowWidthCorrection : 0));
+ element.width(windowWidth - (opts.windowWidthCorrection ? opts.windowWidthCorrection : 0));
+ return true;
+ };
+
+ angular.extend(opts, resizableConfig, scope.resizable);
+
+ if (!angular.element.fn || !angular.element.fn.jquery) {
+ $log.error('resizable: jQuery should be included before AngularJS!');
+ return;
+ }
+
+ recalculateWidths(opts);
+
+ // Create resizable
+ element.resizable(opts);
+
+
+ if (opts.mirror) {
+ breakIfNeeded(opts);
+
+ element.on('resizestart', function () {
+ // stores the width with padding as when setting back the padding is stripped out (setting with
+ // outerWidth does not help at all)
+ // this is probably bug in jQuery and there should be a mechanism to check if it isn't fix yet
+ jQuery(opts.mirror).data('resizestartwidth', jQuery(opts.mirror).outerWidth())
+ });
+ element.on('resize', function (event, ui) {
+ var delta, newWidth;
+ delta = ui.originalSize.width - ui.size.width;
+ if (!delta) {
+ event.preventDefault();
+ event.stopPropagation();
+ return false;
+ }
+ newWidth = jQuery(opts.mirror).data('resizestartwidth') + delta - 1;
+
+ jQuery(opts.mirror).width(newWidth);
+
+ $rootScope.$broadcast('infiniteTableRedraw');
+
+ event.stopPropagation();
+ });
+ jQuery($window).on('resize', function () {
+ var parentWidth, elementWidth, newWidth;
+
+ if (breakIfNeeded(opts)) {
+ return;
+ }
+
+ recalculateWidths(opts);
+
+ parentWidth = element.parent().innerWidth();
+ elementWidth = element.outerWidth();
+
+ if (opts.minWidth && elementWidth < opts.minWidth) {
+ element.width(opts.minWidth);
+ elementWidth = element.outerWidth();
+ } else if (opts.maxWidth && elementWidth > opts.maxWidth) {
+ element.width(opts.maxWidth);
+ elementWidth = element.outerWidth();
+ }
+
+ newWidth = parentWidth - elementWidth - (opts.windowWidthCorrection ? opts.windowWidthCorrection : 1);
+
+ jQuery(opts.mirror).width(newWidth);
+ })
+ }
+ }
+ };
+ }
+ ]);
})(window, window.angular);
\ No newline at end of file
diff --git a/ModelCatalogueCorePlugin/grails-app/assets/stylesheets/modelcatalogue/core/ui/bs/directives.less b/ModelCatalogueCorePlugin/grails-app/assets/stylesheets/modelcatalogue/core/ui/bs/directives.less
index d670fa03fd..ac807c4bdf 100644
--- a/ModelCatalogueCorePlugin/grails-app/assets/stylesheets/modelcatalogue/core/ui/bs/directives.less
+++ b/ModelCatalogueCorePlugin/grails-app/assets/stylesheets/modelcatalogue/core/ui/bs/directives.less
@@ -346,9 +346,7 @@ a div.panel-footer, .with-pointer {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
@media (min-width: 1200px) {
- &:hover, &:active, &:focus, &.expanded {
- width: 400px;
- }
+ width: 400px!important;
}
}
@@ -541,6 +539,11 @@ tr.archived, .catalogue-element-treeview-text-content.archived {
font-style: italic;
}
+tr.inherited, .catalogue-element-treeview-text-content.inherited {
+ font-style: italic;
+ background-color: whitesmoke;
+}
+
.ordered-map-container {
margin-top: 15px;
margin-left: 15px;
diff --git a/ModelCatalogueCorePlugin/grails-app/conf/BuildConfig.groovy b/ModelCatalogueCorePlugin/grails-app/conf/BuildConfig.groovy
index b13431968f..4dd96f998f 100644
--- a/ModelCatalogueCorePlugin/grails-app/conf/BuildConfig.groovy
+++ b/ModelCatalogueCorePlugin/grails-app/conf/BuildConfig.groovy
@@ -38,6 +38,7 @@ grails.project.dependency.resolution = {
//mavenRepo "http://repository.jboss.com/maven2/"
mavenRepo 'http://jcenter.bintray.com'
mavenRepo "http://dl.bintray.com/metadata/model-catalogue"
+ mavenRepo "http://dl.bintray.com/musketyr/document-builder"
//mavenRepo "http://dl.dropbox.com/u/326301/repository"
//mavenRepo "http://www.biojava.org/download/maven/"
@@ -57,7 +58,17 @@ grails.project.dependency.resolution = {
compile "org.modelcatalogue:mc-integration-xml:$mcToolkitVersion"
test "org.modelcatalogue:mc-builder-xml:$mcToolkitVersion"
-
+ compile 'com.craigburke.document:word:0.4.10-fix31'
+
+ compile 'org.jsoup:jsoup:1.8.3'
+
+ compile 'org.modelcatalogue:spreadsheet-builder-poi:0.1.9'
+ compile 'org.modelcatalogue:letter-annotator-lucene:0.2.0'
+
+ compile 'org.apache.poi:poi:3.13'
+ compile 'org.apache.poi:poi-ooxml:3.13'
+ compile 'org.apache.poi:ooxml-schemas:1.1'
+
test 'xmlunit:xmlunit:1.6'
test "org.grails:grails-datastore-test-support:1.0.2-grails-2.4"
@@ -72,9 +83,9 @@ grails.project.dependency.resolution = {
runtime ':database-migration:1.3.6'
- compile ":asset-pipeline:1.9.9"
- compile ":coffee-asset-pipeline:1.9.0"
- compile ":less-asset-pipeline:1.10.0"
+ compile ":asset-pipeline:2.4.3"
+ compile ":coffee-asset-pipeline:2.0.7"
+ compile ":less-asset-pipeline:2.3.0"
// runtime ":hibernate4:4.3.5.5"
runtime ":hibernate:3.6.10.18"
diff --git a/ModelCatalogueCorePlugin/grails-app/conf/ModelCatalogueCorePluginUrlMappings.groovy b/ModelCatalogueCorePlugin/grails-app/conf/ModelCatalogueCorePluginUrlMappings.groovy
index 7fb1745b6a..f35f2fa82e 100644
--- a/ModelCatalogueCorePlugin/grails-app/conf/ModelCatalogueCorePluginUrlMappings.groovy
+++ b/ModelCatalogueCorePlugin/grails-app/conf/ModelCatalogueCorePluginUrlMappings.groovy
@@ -85,13 +85,18 @@ class ModelCatalogueCorePluginUrlMappings {
// /ModelCatalogueCorePluginTestApp/api/modelCatalogue/core/classification/24/report
"/api/modelCatalogue/core/$controllerName/$id/report"(controller: controllerName, action: 'report', method: HttpMethod.GET)
"/api/modelCatalogue/core/$controllerName/$id/gereport"(controller: controllerName, action: 'gereport', method: HttpMethod.GET)
-
}
if (controllerName == 'measurementUnit') {
"/api/modelCatalogue/core/$controllerName/$id/valueDomain"(controller: controllerName, action: 'valueDomains', method: HttpMethod.GET)
}
+ if (controllerName == 'model') {
+ "/api/modelCatalogue/core/gel/reports/inventoryDoc"(controller: 'model', action: 'inventoryDoc', method: HttpMethod.GET)
+ "/api/modelCatalogue/core/gel/reports/classificationChangelog"(controller: 'model', action: 'changelogDoc', method: HttpMethod.GET)
+ "/api/modelCatalogue/core/gel/reports/inventorySpreadsheet"(controller: 'model', action: 'inventorySpreadsheet', method: HttpMethod.GET)
+ }
+
if (controllerName == 'valueDomain') {
"/api/modelCatalogue/core/$controllerName/$id/dataElement"(controller: controllerName, action: 'dataElements', method: HttpMethod.GET)
"/api/modelCatalogue/core/$controllerName/$id/convert/$destination"(controller: controllerName, action: 'convert', method: HttpMethod.GET)
@@ -133,6 +138,7 @@ class ModelCatalogueCorePluginUrlMappings {
"/generateSuggestions" (controller: "dataArchitect", action: "generateSuggestions", method: HttpMethod.POST)
"/suggestionsNames" (controller: "dataArchitect", action: "suggestionsNames", method: HttpMethod.GET)
"/imports/upload" (controller: "dataImport", action: 'upload', method: HttpMethod.POST)
+ "/imports/annotate"(controller: 'dataImport', action: 'annotate', method: HttpMethod.POST)
}
"/"(view:"index")
diff --git a/ModelCatalogueCorePlugin/grails-app/controllers/org/modelcatalogue/core/DataImportController.groovy b/ModelCatalogueCorePlugin/grails-app/controllers/org/modelcatalogue/core/DataImportController.groovy
index d93cdbb319..f4bf0bad38 100644
--- a/ModelCatalogueCorePlugin/grails-app/controllers/org/modelcatalogue/core/DataImportController.groovy
+++ b/ModelCatalogueCorePlugin/grails-app/controllers/org/modelcatalogue/core/DataImportController.groovy
@@ -1,6 +1,7 @@
package org.modelcatalogue.core
import org.modelcatalogue.core.api.ElementStatus
+import org.modelcatalogue.core.util.ClassificationFilter
import org.modelcatalogue.integration.excel.ExcelLoader
import org.modelcatalogue.integration.excel.HeadersMap
import org.modelcatalogue.core.dataarchitect.xsd.XsdLoader
@@ -23,6 +24,7 @@ class DataImportController {
def classificationService
def assetService
def auditService
+ def letterAnnotatorService
private static final CONTENT_TYPES = ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/octet-stream', 'application/xml', 'text/xml']
@@ -35,7 +37,11 @@ class DataImportController {
params.name = file.originalFilename
}
if (!params?.name) errors.add("no import name")
- if (!file) errors.add("no file")
+ if (!file) {
+ errors.add("no file")
+ } else if (file.size <= 0) {
+ errors.add("file is empty")
+ }
return errors
}
@@ -44,7 +50,48 @@ class DataImportController {
return string
}
+ def annotate() {
+ if (!modelCatalogueSecurityService.hasRole('CURATOR')) {
+ render status: HttpStatus.UNAUTHORIZED
+ return
+ }
+
+ if (!(request instanceof MultipartHttpServletRequest)) {
+ respond "errors": [message: 'No file selected']
+ return
+ }
+
+ def errors = []
+
+ MultipartFile file = request.getFile("file")
+ errors.addAll(getErrors(params, file))
+
+
+ Set
classifications = (params.classifications ?: '').split(',').collect{ Long.valueOf(it,10) }.toSet()
+
+ if (!classifications) {
+ errors << "no classifications"
+ }
+
+ if (errors) {
+ respond("errors": errors)
+ return
+ }
+
+
+
+ String letter = file.inputStream.text
+ def id = assetService.storeReportAsAsset(
+ name: params.name,
+ originalFileName: params.name.endsWith('.html') ? params.name : "${params.name}.annotated.html",
+ contentType: "text/html",
+ description: "Your annotated letter will be available soon. Use Refresh action to reload the screen."
+ ) { OutputStream out ->
+ letterAnnotatorService.annotateLetter(classifications.collect{ Classification.get(it)}.toSet(), letter, out)
+ }
+ redirectToAsset(id)
+ }
def upload() {
if (!modelCatalogueSecurityService.hasRole('CURATOR')) {
@@ -211,7 +258,6 @@ class DataImportController {
}
if (!CONTENT_TYPES.contains(confType)) errors.add("input should be an Excel file but uploaded content is ${confType}")
- if (file.size <= 0) errors.add("The uploaded file is empty")
respond "errors": errors
}
diff --git a/ModelCatalogueCorePlugin/grails-app/controllers/org/modelcatalogue/core/ModelController.groovy b/ModelCatalogueCorePlugin/grails-app/controllers/org/modelcatalogue/core/ModelController.groovy
index 6824c1be2e..9576b6c9be 100644
--- a/ModelCatalogueCorePlugin/grails-app/controllers/org/modelcatalogue/core/ModelController.groovy
+++ b/ModelCatalogueCorePlugin/grails-app/controllers/org/modelcatalogue/core/ModelController.groovy
@@ -1,5 +1,8 @@
package org.modelcatalogue.core
+import org.modelcatalogue.core.export.inventory.ModelToDocxExporter
+import org.modelcatalogue.core.export.inventory.ModelToXlsxExporter
+import org.modelcatalogue.core.publishing.changelog.ChangelogGenerator
import org.modelcatalogue.core.util.Lists
class ModelController extends AbstractCatalogueElementController {
@@ -24,4 +27,55 @@ class ModelController extends AbstractCatalogueElementController {
respond Lists.wrap(params, "/${resourceName}/", modelService.getTopLevelModels(params))
}
+ def inventoryDoc() {
+ Model model = Model.get(params.id)
+
+ Long modelId = model.id
+ def assetId= assetService.storeReportAsAsset(
+ name: "${model.name} report as MS Word Document",
+ originalFileName: "${model.name}-${model.status}-${model.version}.docx",
+ contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
+ ) { OutputStream out ->
+ new ModelToDocxExporter(Model.get(modelId), modelService).export(out)
+ }
+
+ response.setHeader("X-Asset-ID",assetId.toString())
+ redirect controller: 'asset', id: assetId, action: 'show'
+ }
+
+
+
+ def inventorySpreadsheet() {
+ Model model = Model.get(params.id)
+
+ Long modelId = model.id
+ def assetId= assetService.storeReportAsAsset(
+ name: "${model.name} report as MS Excel Document",
+ originalFileName: "${model.name}-${model.status}-${model.version}.xlsx",
+ contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+ ) { OutputStream out ->
+ new ModelToXlsxExporter(Model.get(modelId), modelService).export(out)
+ }
+
+ response.setHeader("X-Asset-ID",assetId.toString())
+ redirect controller: 'asset', id: assetId, action: 'show'
+ }
+
+
+ def changelogDoc() {
+ Model model = Model.get(params.id)
+
+ Long modelId = model.id
+ def assetId = assetService.storeReportAsAsset(
+ name: "${model.name} changelog as MS Word Document",
+ originalFileName: "${model.name}-${model.status}-${model.version}-changelog.docx",
+ contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
+ ) { OutputStream out ->
+ new ChangelogGenerator(auditService, modelService).generateChangelog(Model.get(modelId), out)
+ }
+
+ response.setHeader("X-Asset-ID",assetId.toString())
+ redirect controller: 'asset', id: assetId, action: 'show'
+ }
+
}
diff --git a/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/CatalogueElement.groovy b/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/CatalogueElement.groovy
index 228097f22b..fcd1daaafa 100644
--- a/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/CatalogueElement.groovy
+++ b/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/CatalogueElement.groovy
@@ -3,6 +3,7 @@ package org.modelcatalogue.core
import com.google.common.base.Function
import com.google.common.collect.Lists
import grails.util.GrailsNameUtils
+import org.hibernate.proxy.HibernateProxyHelper
import org.modelcatalogue.core.api.ElementStatus
import org.modelcatalogue.core.publishing.DraftContext
import org.modelcatalogue.core.publishing.Published
@@ -11,6 +12,7 @@ import org.modelcatalogue.core.publishing.PublishingChain
import org.modelcatalogue.core.security.User
import org.modelcatalogue.core.util.ExtensionsWrapper
import org.modelcatalogue.core.util.FriendlyErrors
+import org.modelcatalogue.core.util.Inheritance
import org.modelcatalogue.core.util.OrderedMap
import org.modelcatalogue.core.util.RelationshipDirection
@@ -57,7 +59,7 @@ abstract class CatalogueElement implements Extendible, Published
Set outgoingMappings = []
Set incomingMappings = []
- static transients = ['relations', 'info', 'archived', 'relations', 'incomingRelations', 'outgoingRelations', 'defaultModelCatalogueId', 'ext', 'classifications']
+ static transients = ['relations', 'info', 'archived', 'relations', 'incomingRelations', 'outgoingRelations', 'defaultModelCatalogueId', 'ext', 'classifications', 'combinedVersion', 'inheritedAssociationsNames', 'modelCatalogueResourceName']
static hasMany = [incomingRelationships: Relationship, outgoingRelationships: Relationship, outgoingMappings: Mapping, incomingMappings: Mapping, extensions: ExtensionValue]
@@ -149,15 +151,15 @@ abstract class CatalogueElement implements Extendible, Published
[getOutgoingRelationsByType(type), getIncomingRelationsByType(type)].flatten()
}
- List getIncomingRelationshipsByType(RelationshipType type) {
+ List getIncomingRelationshipsByType(RelationshipType type) {
relationshipService.getRelationships([:], RelationshipDirection.INCOMING, this, type).items
}
- List getOutgoingRelationshipsByType(RelationshipType type) {
+ List getOutgoingRelationshipsByType(RelationshipType type) {
relationshipService.getRelationships([:], RelationshipDirection.OUTGOING, this, type).items
}
- List getRelationshipsByType(RelationshipType type) {
+ List getRelationshipsByType(RelationshipType type) {
[getOutgoingRelationshipsByType(type), getIncomingRelationshipsByType(type)].flatten()
}
@@ -266,11 +268,15 @@ abstract class CatalogueElement implements Extendible, Published
}
+ protected String getModelCatalogueResourceName() {
+ fixResourceName GrailsNameUtils.getPropertyName(HibernateProxyHelper.getClassWithoutInitializingProxy(this))
+ }
+
String getDefaultModelCatalogueId(boolean withoutVersion = false) {
if (!grailsLinkGenerator) {
return null
}
- String resourceName = fixResourceName GrailsNameUtils.getPropertyName(getClass())
+ String resourceName = getModelCatalogueResourceName()
if (withoutVersion) {
return grailsLinkGenerator.link(absolute: true, uri: "/catalogue/${resourceName}/${getLatestVersionId() ?: getId()}")
}
@@ -325,6 +331,11 @@ abstract class CatalogueElement implements Extendible, Published
FriendlyErrors.failFriendlySaveWithoutFlush(newOne)
addToExtensions(newOne).save(validate: false)
auditService.logNewMetadata(newOne)
+ Inheritance.withChildren(this) {
+ if (!it.ext.containsKey(name)) {
+ it.addExtension(name, value)
+ }
+ }
return newOne
}
@@ -333,6 +344,12 @@ abstract class CatalogueElement implements Extendible, Published
@Override
void removeExtension(ExtensionValue extension) {
+ Inheritance.withChildren(this) {
+ ExtensionValue oldExt = it.findExtensionByName(extension.name)
+ if (oldExt && oldExt.extensionValue == extension.extensionValue) {
+ it.removeExtension(oldExt)
+ }
+ }
auditService.logMetadataDeleted(extension)
removeFromExtensions(extension).save(validate: false)
extension.delete(flush: true)
@@ -384,8 +401,38 @@ abstract class CatalogueElement implements Extendible, Published
void beforeUpdate() {
auditService.logElementUpdated(this)
+
+ CatalogueElement self = this
+
+
+ if (inheritedAssociationsNames.any { self.isDirty(it) }) {
+ Inheritance.withChildren(this) {
+ boolean changed = false
+
+ for (String propertyName in inheritedAssociationsNames) {
+ if (self.isDirty(propertyName) && it.getProperty(propertyName) == self.getPersistentValue(propertyName)) {
+ it.setProperty(propertyName, self.getProperty(propertyName))
+ changed = true
+ }
+ }
+
+ if (changed) {
+ FriendlyErrors.failFriendlySaveWithoutFlush(it)
+ }
+ }
+
+ for (String propertyName in inheritedAssociationsNames) {
+ if (self.isDirty(propertyName) && self.getProperty(propertyName) == null){
+ Inheritance.withParents(this) {
+ if (it.getProperty(propertyName) != null) {
+ self.setProperty(propertyName, it.getProperty(propertyName))
+ }
+ }
+ }
+ }
+ }
}
-
+
void clearAssociationsBeforeDelete() {
for (Classification c in this.classifications) {
this.removeFromClassifications(c)
@@ -409,6 +456,12 @@ abstract class CatalogueElement implements Extendible, Published
}
ExtensionValue updateExtension(ExtensionValue old, String value) {
+ Inheritance.withChildren(this) {
+ ExtensionValue oldExt = it.findExtensionByName(old.name)
+ if (oldExt && oldExt.extensionValue == old.extensionValue) {
+ it.updateExtension(oldExt, value)
+ }
+ }
old.orderIndex = System.currentTimeMillis()
if (old.extensionValue == value) {
FriendlyErrors.failFriendlySaveWithoutFlush(old)
@@ -473,4 +526,28 @@ abstract class CatalogueElement implements Extendible, Published
org.modelcatalogue.core.api.Relationship removeLinkFrom(org.modelcatalogue.core.api.CatalogueElement source, org.modelcatalogue.core.api.RelationshipType type) {
removeLinkFrom(source as CatalogueElement, type as RelationshipType)
}
+
+ final void addInheritedAssociations(CatalogueElement child) {
+ for (String propertyName in inheritedAssociationsNames) {
+ if (child.getProperty(propertyName) == null) {
+ child.setProperty(propertyName, getProperty(propertyName))
+ }
+ }
+ FriendlyErrors.failFriendlySave(child)
+ }
+
+ final void removeInheritedAssociations(CatalogueElement child) {
+ for (String propertyName in inheritedAssociationsNames) {
+ if (child.getProperty(propertyName) == getProperty(propertyName)) {
+ child.setProperty(propertyName, null)
+ }
+ }
+ FriendlyErrors.failFriendlySave(child)
+ }
+
+ List getInheritedAssociationsNames() { Collections.emptyList() }
+
+ String getCombinedVersion() {
+ "${getLatestVersionId() ?: getId() ?: ''}.${getVersionNumber()}"
+ }
}
\ No newline at end of file
diff --git a/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/DataElement.groovy b/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/DataElement.groovy
index 74a4223d67..823ae76f94 100644
--- a/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/DataElement.groovy
+++ b/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/DataElement.groovy
@@ -42,4 +42,9 @@ class DataElement extends CatalogueElement {
protected PublishingChain prepareDraftChain(PublishingChain chain) {
chain.add(this.containedIn).add(this.classifications)
}
+
+ @Override
+ List getInheritedAssociationsNames() {
+ ['valueDomain']
+ }
}
diff --git a/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/Relationship.groovy b/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/Relationship.groovy
index 7d6fc8c49a..ae47b735c5 100644
--- a/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/Relationship.groovy
+++ b/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/Relationship.groovy
@@ -2,6 +2,7 @@ package org.modelcatalogue.core
import org.modelcatalogue.core.util.ExtensionsWrapper
import org.modelcatalogue.core.util.FriendlyErrors
+import org.modelcatalogue.core.util.Inheritance
import org.modelcatalogue.core.util.OrderedMap
/*
@@ -15,7 +16,6 @@ import org.modelcatalogue.core.util.OrderedMap
| Source | Relationship | Destination | Source->Destination | Destination<-Source |
| ---------------- | -----------------| ----------- | ---------------------- | -------------------- |
| Model | [containment] | DataElement | "contains" | "contained in" |
- | DataElement | [instantiation] | ValueDomain | "instantiated by" | "instantiates" |
| Model | [heirachical] | Model | "parentOf" | "ChildOf" |
| DataElement | [supersession] | DataElement | "supercedes" | "supercededBy" |
@@ -26,7 +26,15 @@ import org.modelcatalogue.core.util.OrderedMap
class Relationship implements Extendible, org.modelcatalogue.core.api.Relationship {
+ // when the relationship class is first loaded set the next index to current time in milliseconds
+ static long nextIndex = System.currentTimeMillis()
+
+ static long getNextIndex() {
+ ++nextIndex;
+ }
+
def auditService
+ def relationshipService
CatalogueElement source
CatalogueElement destination
@@ -44,12 +52,18 @@ class Relationship implements Extendible, org.modelcatalog
* and change from the other side would change the view from the opposite side
*/
@Deprecated
- Long combinedIndex = System.currentTimeMillis()
+ Long combinedIndex
+
+ // init the indexes
+ {
+ resetIndexes()
+ }
static hasMany = [extensions: RelationshipMetadata]
static transients = ['ext']
Boolean archived = false
+ Boolean inherited = false
final Map ext = new ExtensionsWrapper(this)
@@ -84,9 +98,10 @@ class Relationship implements Extendible, org.modelcatalog
}
void resetIndexes() {
- outgoingIndex = System.currentTimeMillis()
- incomingIndex = System.currentTimeMillis()
- combinedIndex = System.currentTimeMillis()
+ long nextIndex = getNextIndex()
+ outgoingIndex = nextIndex
+ incomingIndex = nextIndex
+ combinedIndex = nextIndex
}
@Override
@@ -111,6 +126,14 @@ class Relationship implements Extendible, org.modelcatalog
FriendlyErrors.failFriendlySaveWithoutFlush(newOne)
addToExtensions(newOne).save(validate: false)
auditService.logNewRelationshipMetadata(newOne)
+ Inheritance.withChildren(source) {
+ RelationshipDefinition definition = RelationshipDefinition.from(this)
+ definition.source = it
+ Relationship rel = relationshipService.findExistingRelationship(definition)
+ if (rel) {
+ rel.addExtension(name, value)
+ }
+ }
return newOne
}
throw new IllegalStateException("Cannot add extension before saving the element (id: ${getId()}, attached: ${isAttached()})")
@@ -118,6 +141,17 @@ class Relationship implements Extendible, org.modelcatalog
@Override
void removeExtension(RelationshipMetadata extension) {
+ Inheritance.withChildren(source) {
+ RelationshipDefinition definition = RelationshipDefinition.from(this)
+ definition.source = it
+ Relationship rel = relationshipService.findExistingRelationship(definition)
+ if (rel) {
+ RelationshipMetadata existing = rel.findExtensionByName(extension.name)
+ if (existing.extensionValue == extension.extensionValue) {
+ rel.removeExtension(existing)
+ }
+ }
+ }
auditService.logRelationshipMetadataDeleted(extension)
removeFromExtensions(extension).save()
extension.delete(flush: true)
@@ -125,6 +159,17 @@ class Relationship implements Extendible, org.modelcatalog
@Override
RelationshipMetadata updateExtension(RelationshipMetadata old, String value) {
+ Inheritance.withChildren(source) {
+ RelationshipDefinition definition = RelationshipDefinition.from(this)
+ definition.source = it
+ Relationship rel = relationshipService.findExistingRelationship(definition)
+ if (rel) {
+ RelationshipMetadata existing = rel.findExtensionByName(old.name)
+ if (existing.extensionValue == old.extensionValue) {
+ rel.updateExtension(existing, value)
+ }
+ }
+ }
if (old.extensionValue == value) {
old.orderIndex = System.currentTimeMillis()
FriendlyErrors.failFriendlySaveWithoutFlush(old)
diff --git a/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/ValueDomain.groovy b/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/ValueDomain.groovy
index 831ace00f7..00a647ca3b 100644
--- a/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/ValueDomain.groovy
+++ b/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/ValueDomain.groovy
@@ -170,4 +170,9 @@ class ValueDomain extends CatalogueElement {
FriendlyErrors.failFriendlySave(element)
}
}
+
+ @Override
+ List getInheritedAssociationsNames() {
+ ['dataType', 'unitOfMeasure']
+ }
}
diff --git a/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/audit/Change.groovy b/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/audit/Change.groovy
index e5ceb920bd..eab10dc8e4 100644
--- a/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/audit/Change.groovy
+++ b/ModelCatalogueCorePlugin/grails-app/domain/org/modelcatalogue/core/audit/Change.groovy
@@ -53,7 +53,7 @@ class Change {
@Override
String toString() {
- "Change[id: $id, change: $changedId, parent: $parentId, latest: $latestVersionId, author: $authorId, type: $type, property: $property, newValue: $newValue, oldValue: $oldValue, undone: $undone, otherSide: $otherSide]"
+ "Change[id: $id, changed element: $changedId ($latestVersionId), parent change: $parentId, author: $authorId, type: $type, property: $property, newValue: $newValue, oldValue: $oldValue, undone: $undone, otherSide: $otherSide]"
}
boolean undo() {
diff --git a/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/AssetService.groovy b/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/AssetService.groovy
index 0b9c1b9988..781fb8940f 100644
--- a/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/AssetService.groovy
+++ b/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/AssetService.groovy
@@ -1,9 +1,13 @@
package org.modelcatalogue.core
import groovy.transform.CompileStatic
+import groovy.transform.stc.ClosureParams
+import groovy.transform.stc.FromString
import org.apache.commons.io.input.CountingInputStream
import org.apache.commons.io.output.CountingOutputStream
import org.codehaus.groovy.runtime.InvokerInvocationException
+import org.modelcatalogue.core.api.ElementStatus
+import org.modelcatalogue.core.audit.AuditService
import org.modelcatalogue.core.publishing.DraftContext
import org.springframework.util.DigestUtils
import org.springframework.web.multipart.MultipartFile
@@ -11,15 +15,16 @@ import org.springframework.web.multipart.MultipartFile
import java.security.DigestInputStream
import java.security.DigestOutputStream
import java.security.MessageDigest
+import java.util.concurrent.ExecutorService
-/**
- * Created by ladin on 10.07.14.
- */
@CompileStatic
class AssetService {
StorageService modelCatalogueStorageService
ElementService elementService
+ ExecutorService executorService
+ SecurityService modelCatalogueSecurityService
+ AuditService auditService
private static final long GIGA = 1024 * 1024 * 1024
private static final long MEGA = 1024 * 1024
@@ -153,4 +158,44 @@ class AssetService {
asset.save(flush: true)
}
+ Long storeReportAsAsset(Map assetParams, @ClosureParams(value = FromString, options= "java.io.OutputStream") Closure worker){
+ assert assetParams.name
+ assert assetParams.contentType
+ assert assetParams.originalFileName
+
+ assetParams.size = 0
+ assetParams.status = ElementStatus.PENDING
+ assetParams.description = assetParams.description ?: "Your report will be available in this asset soon. Use Refresh action to reload"
+
+ Asset asset = new Asset(assetParams)
+
+ asset.save(flush: true, failOnError: true)
+
+ Long id = asset.id
+ Long authorId = modelCatalogueSecurityService.currentUser?.id
+
+ executorService.submit {
+ auditService.withDefaultAuthorId(authorId) {
+ Asset updated = Asset.get(id)
+ try {
+ //do the hard work
+ storeAssetWithSteam(updated, assetParams.contentType, worker)
+
+ updated.status = ElementStatus.FINALIZED
+ updated.description = "Your report is ready. Use Download button to download it."
+ updated.save(flush: true, failOnError: true)
+ } catch (e) {
+ log.error "Exception of type ${e.class} with id=${id}", e
+
+ updated.refresh()
+ updated.status = ElementStatus.FINALIZED
+ updated.name = updated.name + " - Error during generation"
+ updated.description = "Error generating report" +":$e"
+ updated.save(flush: true, failOnError: true)
+ }
+ }
+ }
+ return asset.id;
+ }
+
}
diff --git a/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/ClassificationService.groovy b/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/ClassificationService.groovy
index e187c4872d..17274a665f 100644
--- a/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/ClassificationService.groovy
+++ b/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/ClassificationService.groovy
@@ -37,7 +37,11 @@ class ClassificationService {
return list
}
- public DetachedCriteria classified(DetachedCriteria criteria, ClassificationFilter classificationsFilter = classificationsInUse) {
+ public DetachedCriteria classified(DetachedCriteria criteria) {
+ classified(criteria, classificationsInUse)
+ }
+
+ public static DetachedCriteria classified(DetachedCriteria criteria, ClassificationFilter classificationsFilter) {
if (criteria.persistentEntity.javaClass == Classification) {
return criteria
}
diff --git a/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/LetterAnnotatorService.groovy b/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/LetterAnnotatorService.groovy
new file mode 100644
index 0000000000..946df230a9
--- /dev/null
+++ b/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/LetterAnnotatorService.groovy
@@ -0,0 +1,137 @@
+package org.modelcatalogue.core
+
+import org.modelcatalogue.core.util.ClassificationFilter
+import org.modelcatalogue.letter.annotator.AnnotatedLetter
+import org.modelcatalogue.letter.annotator.CandidateTerm
+import org.modelcatalogue.letter.annotator.Highlighter
+import org.modelcatalogue.letter.annotator.LetterAnnotator
+import org.modelcatalogue.letter.annotator.TermOccurrence
+import org.modelcatalogue.letter.annotator.lucene.LuceneLetterAnnotator
+
+class LetterAnnotatorService {
+
+ static transactional = false
+
+ ClassificationService classificationService
+
+ def annotateLetter(Set classifications, String letter, OutputStream assetStream) {
+ // language=HTML
+ assetStream << """
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
"""
+
+ LetterAnnotator annotator = new LuceneLetterAnnotator()
+
+ for (Classification classification in classifications) {
+ List models = classificationService.classified(Model, ClassificationFilter.includes(classification)).list(
+ sort: 'versionNumber',
+ order: 'desc'
+ )
+
+ models.eachWithIndex { Model m, idx ->
+ log.info "[${idx + 1}/${models.size()}] Adding $m.name to index"
+ annotator.addCandidate(CandidateTerm.create(m.name).with { CandidateTerm.Builder builder ->
+ description m.description
+ url(m.getDefaultModelCatalogueId(true))
+ title "$m.name ($classification.name)"
+ extensions 'data-toggle', 'popover'
+ extensions 'id', m.getId()
+ extensions 'data-classification', classification.name
+ builder
+ });
+ }
+
+ }
+
+ log.info "Finding candidate terms"
+ AnnotatedLetter annotatedLetter = annotator.annotate(letter)
+
+ assetStream << annotatedLetter.text
+ log.info "Annotated letter exported"
+
+ assetStream << """
+
+
+ """
+
+ if (annotatedLetter.occurrences) {
+ assetStream << """
+
+
+
Terms Occurrences
+
+ """
+
+ for (TermOccurrence occurrence in annotatedLetter.occurrences) {
+ assetStream << """"""
+
+ assetStream << """${occurrence.positiveOccurrence} | """
+
+ assetStream << """
+ ${occurrence.term.term}
+ """
+
+ if (occurrence.term.url) {
+ assetStream << """ """
+ }
+
+ if (occurrence.term.extensions.'data-classification') {
+ assetStream << """ ${occurrence.term.extensions.'data-classification'}"""
+ }
+
+ assetStream << """ | """
+
+
+ assetStream << """
"""
+ }
+
+ assetStream << """
+
+
+ """
+ }
+
+ assetStream << """
+
+
+ """
+ }
+}
diff --git a/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/ModelService.groovy b/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/ModelService.groovy
index 8f2255baf9..143144f3ed 100644
--- a/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/ModelService.groovy
+++ b/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/ModelService.groovy
@@ -129,8 +129,8 @@ class ModelService {
}
ListWithTotalAndType getSubModels(Model model) {
- List models = listChildren(model)
- new ListCountAndType(count: models.size(), list: models, itemType: Model)
+ Map models = collectChildren(3, model, new TreeMap())
+ new ListCountAndType(count: models.size(), list: models.values().toList(), itemType: Model)
}
@@ -143,14 +143,24 @@ class ModelService {
}
- protected List listChildren(Model model, results = []){
- if (model && !results.contains(model)) {
- results += model
- model.parentOf?.each { child ->
- results += listChildren(child, results)
- }
+ protected Map collectChildren(int maxLevel, Model model, Map results) {
+ log.info "Collecting children for $model.name ($model.combinedVersion)"
+ String key = "$model.name $model.combinedVersion".toString()
+ if (model && !results.containsKey(key)) {
+ results[key] = model
+ if (maxLevel > 0) {
+ model.parentOf?.each { Model child ->
+ collectChildren(maxLevel - 1, child, results)
+ }
+ } else {
+ int count = model.countParentOf()
+ if (count > 0) {
+ log.info "Reached max level for travelsal at $model.name ($model.combinedVersion) with $count child models left"
+ }
}
- results.unique()
+
+ }
+ results
}
diff --git a/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/RelationshipService.groovy b/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/RelationshipService.groovy
index 558ae0cba6..b1c24eb64d 100644
--- a/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/RelationshipService.groovy
+++ b/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/RelationshipService.groovy
@@ -6,6 +6,7 @@ import org.modelcatalogue.core.api.ElementStatus
import org.modelcatalogue.core.security.User
import org.modelcatalogue.core.util.ClassificationFilter
import org.modelcatalogue.core.util.FriendlyErrors
+import org.modelcatalogue.core.util.Inheritance
import org.modelcatalogue.core.util.ListWithTotal
import org.modelcatalogue.core.util.Lists
import org.modelcatalogue.core.util.RelationshipDirection
@@ -68,7 +69,7 @@ class RelationshipService {
}
Relationship link(RelationshipDefinition relationshipDefinition) {
- if (relationshipDefinition.source?.id && relationshipDefinition.destination?.id && relationshipDefinition.relationshipType?.id) {
+ if (relationshipDefinition.source?.readyForQueries && relationshipDefinition.destination?.readyForQueries && relationshipDefinition.relationshipType?.getId()) {
Relationship relationshipInstance = relationshipDefinition.skipUniqueChecking ? null : findExistingRelationship(relationshipDefinition)
if (relationshipInstance) {
@@ -129,7 +130,37 @@ class RelationshipService {
}
log.debug "Created $relationshipDefinition"
-
+
+
+ if (relationshipDefinition.relationshipType == RelationshipType.baseType) {
+ // copy relationships when new based on relationship is created
+ for (Relationship relationship in new LinkedHashSet(relationshipDefinition.source.outgoingRelationships)) {
+ if (relationship.relationshipType.versionSpecific) {
+ RelationshipDefinition newDefinition = RelationshipDefinition.from(relationship)
+ newDefinition.source = relationshipDefinition.destination
+ newDefinition.inherited = true
+ Relationship newRelationship = link newDefinition
+ if (newRelationship.hasErrors()) {
+ relationshipInstance.errors.reject('unable.to.copy.from.parent', FriendlyErrors.printErrors("Unable to copy relationship $newDefinition from ${relationshipDefinition.source} to child ${relationshipDefinition.destination}", newRelationship.errors))
+ }
+ }
+ }
+ relationshipDefinition.destination.ext.putAll relationshipDefinition.source.ext.subMap(relationshipDefinition.source.ext.keySet() - relationshipDefinition.destination.ext.keySet())
+ relationshipDefinition.source.addInheritedAssociations(relationshipDefinition.destination)
+ } else if (relationshipDefinition.relationshipType.versionSpecific) {
+ // propagate relationship to the children
+ Inheritance.withChildren(relationshipInstance.source) {
+ RelationshipDefinition newDefinition = relationshipDefinition.clone()
+ newDefinition.source = it
+ newDefinition.inherited = true
+ Relationship newRelationship = link newDefinition
+ if (newRelationship.hasErrors()) {
+ relationshipInstance.errors.reject('unable.to.copy.from.parent', FriendlyErrors.printErrors("Unable to propagate relationship $newDefinition from ${relationshipDefinition.source} to child ${newDefinition.source}", newRelationship.errors))
+ }
+ }
+ }
+
+
relationshipInstance
}
@@ -182,26 +213,70 @@ class RelationshipService {
unlink source, destination, relationshipType, null, ignoreRules
}
- Relationship unlink(CatalogueElement source, CatalogueElement destination, RelationshipType relationshipType, Classification classification, boolean ignoreRules = false) {
+ Relationship unlink(CatalogueElement source, CatalogueElement destination, RelationshipType relationshipType, Classification classification, boolean ignoreRules = false, Map expectedMetadata = null) {
if (source?.id && destination?.id && relationshipType?.id) {
Relationship relationshipInstance = findExistingRelationship(RelationshipDefinition.create(source, destination, relationshipType).withClassification(classification).definition)
+ if (!relationshipInstance) {
+ return null
+ }
+
if(!ignoreRules) {
if (relationshipType.versionSpecific && !relationshipType.system && source.status != ElementStatus.DRAFT && source.status != ElementStatus.UPDATED && source.status != ElementStatus.DEPRECATED) {
relationshipInstance.errors.rejectValue('relationshipType', 'org.modelcatalogue.core.RelationshipType.sourceClass.finalizedDataElement.remove', [source.status.toString()] as Object[], "Cannot changed finalized elements.")
return relationshipInstance
}
+
+ if (relationshipInstance.inherited) {
+ relationshipInstance.errors.rejectValue('inherited', 'org.modelcatalogue.core.RelationshipType.cannot.change.inherited', "Cannot changed inherited relationships.")
+ return relationshipInstance
+ }
+
}
- if (relationshipInstance && source && destination) {
- auditService.logRelationRemoved(relationshipInstance)
- destination?.removeFromIncomingRelationships(relationshipInstance)
- source?.removeFromOutgoingRelationships(relationshipInstance)
- relationshipInstance.classification = null
- relationshipInstance.delete(flush: true)
- return relationshipInstance
+ if (expectedMetadata != null && expectedMetadata != relationshipInstance.ext) {
+ return null
+ }
+
+ auditService.logRelationRemoved(relationshipInstance)
+
+ destination.refresh()
+ source.refresh()
+
+ if (relationshipType == RelationshipType.baseType) {
+ for (Relationship relationship in new LinkedHashSet(source.outgoingRelationships)) {
+ if (relationship.relationshipType.versionSpecific) {
+ unlink relationship.source, relationship.destination, relationship.relationshipType, relationship.classification, true, relationship.ext
+ }
+ }
+ List forRemoval = []
+ source.ext.each { String key, String value ->
+ String valueInChild = destination.ext[key]
+ if (valueInChild == value) {
+ forRemoval << key
+ }
+ }
+ forRemoval.each {
+ source.ext.remove(it)
+ }
+ } else if (relationshipType.versionSpecific) {
+ Inheritance.withChildren(source) {
+ unlink(it, destination, relationshipType, classification, true, relationshipInstance.ext)
+ }
}
+
+ destination.removeFromIncomingRelationships(relationshipInstance)
+ source.removeFromOutgoingRelationships(relationshipInstance)
+ relationshipInstance.source = null
+ relationshipInstance.destination = null
+ relationshipInstance.classification = null
+ relationshipInstance.delete(flush: true)
+ if (relationshipType == RelationshipType.baseType) {
+ source.removeInheritedAssociations(destination)
+ }
+
+ return relationshipInstance
}
return null
}
diff --git a/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/audit/AuditService.groovy b/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/audit/AuditService.groovy
index 56342e5715..eebab2b82c 100644
--- a/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/audit/AuditService.groovy
+++ b/ModelCatalogueCorePlugin/grails-app/services/org/modelcatalogue/core/audit/AuditService.groovy
@@ -1,5 +1,6 @@
package org.modelcatalogue.core.audit
+import grails.gorm.DetachedCriteria
import grails.util.Holders
import org.hibernate.SessionFactory
import org.modelcatalogue.core.*
@@ -179,18 +180,23 @@ class AuditService {
where ce.id in (select distinct r.destination.id from Relationship r where r.relationshipType = :classificationType and r.source.id in (:includes)) or ce.id in (:includes)
"""
}
- throw new IllegalArgumentException("Classification fitler must be set")
+ throw new IllegalArgumentException("Classification filter must be set")
}
- ListWithTotalAndType getChanges(Map params, CatalogueElement element) {
+ ListWithTotalAndType getChanges(Map params, CatalogueElement element, @DelegatesTo(DetachedCriteria) Closure additionalCriteria = {}) {
if (!params.sort) {
params.sort = 'dateCreated'
params.order = 'desc'
}
- Lists.fromCriteria(params, Change) {
+
+ DetachedCriteria criteria = new DetachedCriteria(Change).build {
eq 'changedId', element.id
ne 'system', Boolean.TRUE
- }
+ }.build additionalCriteria
+
+
+
+ Lists.fromCriteria(params, criteria)
}
ListWithTotalAndType getSubChanges(Map params, Change change) {
diff --git a/ModelCatalogueCorePlugin/scripts/_Events.groovy b/ModelCatalogueCorePlugin/scripts/_Events.groovy
index bdb2793c7b..b6f0c6d656 100644
--- a/ModelCatalogueCorePlugin/scripts/_Events.groovy
+++ b/ModelCatalogueCorePlugin/scripts/_Events.groovy
@@ -1,7 +1,18 @@
+import grails.util.Metadata
+
eventCleanStart = { args ->
- File tmpFolder = new File("${System.getProperty('java.io.tmpdir')}/mc")
+ File tmpFolder = new File("${System.getProperty('java.io.tmpdir')}/${Metadata.getCurrent().getApplicationName()}/${Metadata.getCurrent().getApplicationVersion()}")
if (tmpFolder.exists() && tmpFolder.directory) {
println "\nRemoving old test databases from previous runs\n"
tmpFolder.deleteDir()
}
}
+
+eventTestCaseStart = { name ->
+ println '-' * 60
+ println "|$name : started"
+}
+
+eventTestCaseEnd = { name, err, out ->
+ println "\n|$name : finished"
+}
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/RelationshipDefinition.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/RelationshipDefinition.groovy
index 02236ba97c..60c62441a2 100644
--- a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/RelationshipDefinition.groovy
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/RelationshipDefinition.groovy
@@ -9,6 +9,19 @@ class RelationshipDefinition {
return new RelationshipDefinitionBuilder(new RelationshipDefinition(source, destination, relationshipType))
}
+ static RelationshipDefinition from(Relationship relationship) {
+ RelationshipDefinition definition = new RelationshipDefinition(relationship.source, relationship.destination, relationship.relationshipType)
+ definition.with {
+ classification = relationship.classification
+ metadata = new LinkedHashMap(relationship.ext)
+ archived = relationship.archived
+ outgoingIndex = relationship.outgoingIndex
+ incomingIndex = relationship.incomingIndex
+ combinedIndex = relationship.combinedIndex
+ }
+ definition
+ }
+
// required
CatalogueElement source
CatalogueElement destination
@@ -28,6 +41,7 @@ class RelationshipDefinition {
Classification classification = null
Map metadata = [:]
boolean archived
+ boolean inherited
Long outgoingIndex
Long incomingIndex
Long combinedIndex
@@ -44,6 +58,7 @@ class RelationshipDefinition {
relationshipType: relationshipType?.id ? relationshipType : null,
classification: classification?.id ? classification : null,
archived: archived,
+ inherited: inherited,
outgoingIndex: outgoingIndex ?: System.currentTimeMillis(),
incomingIndex: incomingIndex ?: System.currentTimeMillis(),
combinedIndex: combinedIndex ?: System.currentTimeMillis()
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/audit/ChangeType.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/audit/ChangeType.groovy
index d6e1e28b57..5b819f0477 100644
--- a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/audit/ChangeType.groovy
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/audit/ChangeType.groovy
@@ -5,9 +5,6 @@ import org.apache.log4j.Logger
import org.modelcatalogue.core.*
import org.modelcatalogue.core.util.FriendlyErrors
-/**
- * Created by ladin on 17.02.15.
- */
enum ChangeType {
NEW_ELEMENT_CREATED {
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/audit/DefaultAuditor.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/audit/DefaultAuditor.groovy
index 19af8947b6..7c5a3a1906 100644
--- a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/audit/DefaultAuditor.groovy
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/audit/DefaultAuditor.groovy
@@ -454,7 +454,7 @@ class DefaultAuditor implements Auditor {
if (object.elementType == Relationship.name) {
return object
}
- return Class.forName(object.elementType).get(object.id)
+ return Class.forName(CatalogueElement.fixResourceName(object.elementType)).get(object.id)
}
throw new IllegalArgumentException("Unsupported stored value: $string")
}
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/comments/Comment.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/comments/Comment.groovy
new file mode 100644
index 0000000000..a4c5403591
--- /dev/null
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/comments/Comment.groovy
@@ -0,0 +1,11 @@
+package org.modelcatalogue.core.comments
+
+class Comment {
+
+ String text
+ String username
+ Date created
+
+ Object original
+
+}
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/comments/CommentsService.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/comments/CommentsService.groovy
new file mode 100644
index 0000000000..aff45604c2
--- /dev/null
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/comments/CommentsService.groovy
@@ -0,0 +1,12 @@
+package org.modelcatalogue.core.comments
+
+import org.modelcatalogue.core.CatalogueElement
+
+
+public interface CommentsService {
+
+ List getComments(CatalogueElement element)
+
+ boolean isForumEnabled()
+
+}
\ No newline at end of file
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/ddl/CreateDefinition.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/ddl/CreateDefinition.groovy
new file mode 100644
index 0000000000..7b6281d334
--- /dev/null
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/ddl/CreateDefinition.groovy
@@ -0,0 +1,46 @@
+package org.modelcatalogue.core.ddl
+
+import grails.util.GrailsNameUtils
+import org.modelcatalogue.core.CatalogueElement
+import org.modelcatalogue.core.util.FriendlyErrors
+
+class CreateDefinition {
+
+ final DataDefinitionLanguage ddl
+ final Class domain
+
+ CreateDefinition(DataDefinitionLanguage ddl, Class domain) {
+ this.ddl = ddl
+ this.domain = domain
+ }
+
+ void called(Map props = [:], String name) {
+ T existing = null
+
+ try {
+ existing = ddl.find(domain, name)
+ } catch(IllegalArgumentException ignored) {}
+
+ if (existing) {
+ throw new IllegalArgumentException("${GrailsNameUtils.getNaturalName(domain.simpleName)} called '${name}' already exists")
+ }
+
+ T element = domain.newInstance()
+ element.name = name
+
+ for (Map.Entry entry in props) {
+ if (element.hasProperty(entry.key)) {
+ element.setProperty(entry.key, entry.value)
+ } else {
+ element.ext[entry.key] = entry.value
+ }
+ }
+
+ FriendlyErrors.failFriendlySave(element)
+
+ if (ddl.classification) {
+ ddl.classification.addToClassifies element
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/ddl/CreateDraftDefinition.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/ddl/CreateDraftDefinition.groovy
new file mode 100644
index 0000000000..c03bef005b
--- /dev/null
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/ddl/CreateDraftDefinition.groovy
@@ -0,0 +1,19 @@
+package org.modelcatalogue.core.ddl
+
+import grails.util.Holders
+import org.modelcatalogue.core.CatalogueElement
+import org.modelcatalogue.core.publishing.DraftContext;
+
+class CreateDraftDefinition {
+
+ final DataDefinitionLanguage ddl
+
+ CreateDraftDefinition(DataDefinitionLanguage ddl) {
+ this.ddl = ddl
+ }
+
+ void of(String name) {
+ Holders.applicationContext.elementService.createDraftVersion(ddl.find(CatalogueElement, name), DraftContext.userFriendly())
+ }
+
+}
\ No newline at end of file
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/ddl/DataDefinitionLanguage.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/ddl/DataDefinitionLanguage.groovy
new file mode 100644
index 0000000000..52ed219860
--- /dev/null
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/ddl/DataDefinitionLanguage.groovy
@@ -0,0 +1,84 @@
+package org.modelcatalogue.core.ddl
+
+import grails.gorm.DetachedCriteria
+import grails.util.GrailsNameUtils
+import grails.util.Holders
+import org.modelcatalogue.core.CatalogueElement
+import org.modelcatalogue.core.Classification
+import org.modelcatalogue.core.ClassificationService
+import org.modelcatalogue.core.ElementService
+import org.modelcatalogue.core.util.ClassificationFilter
+
+/**
+ * Simple data definition language. Designed to be usually used in the tests.
+ */
+class DataDefinitionLanguage {
+
+ static void with(Classification classification, @DelegatesTo(DataDefinitionLanguage) Closure closure) {
+ new DataDefinitionLanguage(classification: classification).with closure
+ }
+ static void with(String classification, @DelegatesTo(DataDefinitionLanguage) Closure closure) {
+ new DataDefinitionLanguage(classification: find(Classification, classification, null)).with closure
+ }
+
+ private Classification classification
+
+ UpdateDefinition update(String propertyOrExtName) {
+ return new UpdateDefinition(this, propertyOrExtName)
+ }
+
+ public CreateDefinition create(Class domain) {
+ return new CreateDefinition(this, domain)
+ }
+
+ CreateDraftDefinition create(DraftKeyword ignored) {
+ return new CreateDraftDefinition(this)
+ }
+
+ void finalize(String name) {
+ Holders.applicationContext.getBean(ElementService).finalizeElement(find(CatalogueElement, name))
+ }
+
+ void deprecate(String name) {
+ Holders.applicationContext.getBean(ElementService).archive(find(CatalogueElement, name), true)
+ }
+
+ static DraftKeyword getDraft() {
+ return DraftKeyword.INSTANCE
+ }
+
+ protected Classification getClassification() {
+ return classification
+ }
+
+ protected T find(Class domain, String name) {
+ find domain, name, classification
+ }
+
+ protected static T find(Class domain, String name, Classification classification) {
+ DetachedCriteria criteria = new DetachedCriteria(domain).build {
+ eq 'name', name
+ }
+
+ if (domain != classification && classification) {
+ criteria = ClassificationService.classified(criteria, ClassificationFilter.includes(classification))
+ }
+
+ List elements = criteria.list(sort: 'versionNumber', order: 'desc')
+ T element = elements ? elements[0] : null
+ if (!element && domain == CatalogueElement) {
+ criteria = new DetachedCriteria(domain).build {
+ eq 'name', name
+ }
+ elements = criteria.list(sort: 'versionNumber', order: 'desc')
+ element = elements ? elements[0] : null
+ if (!element?.instanceOf(Classification)) {
+ element = null
+ }
+ }
+ if (!element) {
+ throw new IllegalArgumentException("${GrailsNameUtils.getNaturalName(domain.simpleName)} '$name' not found!")
+ }
+ return element
+ }
+}
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/ddl/DraftKeyword.java b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/ddl/DraftKeyword.java
new file mode 100644
index 0000000000..a36c1fbab0
--- /dev/null
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/ddl/DraftKeyword.java
@@ -0,0 +1,5 @@
+package org.modelcatalogue.core.ddl;
+
+enum DraftKeyword {
+ INSTANCE
+}
\ No newline at end of file
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/ddl/UpdateAction.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/ddl/UpdateAction.groovy
new file mode 100644
index 0000000000..dc3bad7d1b
--- /dev/null
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/ddl/UpdateAction.groovy
@@ -0,0 +1,54 @@
+package org.modelcatalogue.core.ddl
+
+import grails.util.Holders
+import org.codehaus.groovy.grails.commons.GrailsDomainClass
+import org.codehaus.groovy.grails.commons.GrailsDomainClassProperty
+import org.hibernate.proxy.HibernateProxyHelper
+import org.modelcatalogue.core.CatalogueElement
+import org.modelcatalogue.core.RelationshipType
+import org.modelcatalogue.core.util.FriendlyErrors;
+
+class UpdateAction {
+
+ final DataDefinitionLanguage ddl
+ final String property
+ final CatalogueElement element
+
+ UpdateAction(DataDefinitionLanguage ddl, String property, CatalogueElement element) {
+ this.ddl = ddl
+ this.property = property
+ this.element = element
+ }
+
+ void to(Object newValue) {
+ GrailsDomainClass clazz = Holders.grailsApplication.getDomainClass(HibernateProxyHelper.getClassWithoutInitializingProxy(element).name)
+ if (element.hasProperty(property)) {
+ GrailsDomainClassProperty prop = clazz.getPersistentProperty(property)
+ if (prop.association) {
+ element.setProperty(property, newValue ? ddl.find(prop.referencedDomainClass.clazz, newValue.toString()) : null)
+ } else {
+ element.setProperty(property, newValue)
+ }
+ FriendlyErrors.failFriendlySave(element)
+ return
+ }
+ element.ext.put(property, newValue?.toString())
+ }
+
+ void add(Map ext, String name) {
+ RelationshipType type = RelationshipType.readByName(property)
+ element.createLinkTo(ddl.find(type.destinationClass, name), type, metadata: ext.collectEntries {
+ [it.key, it.value?.toString()]
+ })
+ }
+
+ void add(String name) {
+ RelationshipType type = RelationshipType.readByName(property)
+ element.createLinkTo(ddl.find(type.destinationClass, name), type)
+ }
+
+ void remove(String name) {
+ RelationshipType type = RelationshipType.readByName(property)
+ element.removeLinkTo(ddl.find(type.destinationClass, name), type)
+ }
+}
\ No newline at end of file
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/ddl/UpdateDefinition.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/ddl/UpdateDefinition.groovy
new file mode 100644
index 0000000000..9e5547d72e
--- /dev/null
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/ddl/UpdateDefinition.groovy
@@ -0,0 +1,21 @@
+package org.modelcatalogue.core.ddl
+
+import org.modelcatalogue.core.CatalogueElement
+
+class UpdateDefinition {
+ final DataDefinitionLanguage ddl
+ final String property
+
+ UpdateDefinition(DataDefinitionLanguage ddl, String property) {
+ this.ddl = ddl
+ this.property = property
+ }
+
+ UpdateAction of(String catalogueElementName) {
+ CatalogueElement element = ddl.find CatalogueElement, catalogueElementName
+ if (!element) {
+ throw new IllegalArgumentException("Catalogue element $catalogueElementName not found!")
+ }
+ return new UpdateAction(ddl, property, element)
+ }
+}
\ No newline at end of file
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/export/inventory/ModelCatalogueStyles.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/export/inventory/ModelCatalogueStyles.groovy
new file mode 100644
index 0000000000..9ad06d7d10
--- /dev/null
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/export/inventory/ModelCatalogueStyles.groovy
@@ -0,0 +1,116 @@
+package org.modelcatalogue.core.export.inventory
+
+import org.modelcatalogue.builder.spreadsheet.api.CanDefineStyle
+import org.modelcatalogue.builder.spreadsheet.api.Stylesheet
+
+class ModelCatalogueStyles implements Stylesheet {
+
+ @Override
+ void declareStyles(CanDefineStyle stylable) {
+ stylable.with {
+ style('h1') {
+ align center center
+ font {
+ bold
+ size 22
+ color cornflowerBlue
+ }
+ }
+ style('h2') {
+ align center center
+ font {
+ bold
+ size 16
+ color cornflowerBlue
+ }
+ }
+ style('description') {
+ wrap text
+ align top justify
+ font {
+ color dimGray
+ }
+ }
+ style('date') {
+ format 'dd/mm/yy'
+ align center center
+ font {
+ size 12
+ color dimGray
+ }
+ }
+ style('status') {
+ align center center
+ font {
+ size 12
+ color dimGray
+ }
+ }
+ style('property-value') {
+ align center center
+ font {
+ color dimGray
+ }
+ }
+ style('model-catalogue-id') {
+ align center center
+ font {
+ size 12
+ color dimGray
+ }
+ }
+ style('inner-table-header') {
+ font {
+ bold
+ size 12
+ font {
+ color cornflowerBlue
+ }
+ }
+ align center center
+ }
+ style('note') {
+ font {
+ italic
+ color dimGray
+ align center center
+ }
+ }
+ style('property-title') {
+ font {
+ color dimGray
+ bold
+ }
+ }
+ style('data-element-row') {
+ foreground whiteSmoke
+ font {
+ bold
+ }
+ }
+ style('data-element-description-row') {
+ wrap text
+ font { size 10 }
+ align top justify
+ }
+ style('metadata-key') {
+ font {
+ size 10
+ bold
+ indent 4
+ }
+ }
+
+ style ('metadata-value') {
+ font {
+ size 10
+ }
+ }
+ style ('link') {
+ font {
+ underline
+ }
+ }
+ }
+ }
+}
diff --git a/ModelCatalogueGenomicsPlugin/src/groovy/org/modelcatalogue/core/gel/ClassificationToDocxExporter.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/export/inventory/ModelToDocxExporter.groovy
similarity index 86%
rename from ModelCatalogueGenomicsPlugin/src/groovy/org/modelcatalogue/core/gel/ClassificationToDocxExporter.groovy
rename to ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/export/inventory/ModelToDocxExporter.groovy
index 254869c9c4..2e74d39996 100644
--- a/ModelCatalogueGenomicsPlugin/src/groovy/org/modelcatalogue/core/gel/ClassificationToDocxExporter.groovy
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/export/inventory/ModelToDocxExporter.groovy
@@ -1,22 +1,22 @@
-package org.modelcatalogue.core.gel
+package org.modelcatalogue.core.export.inventory
-import com.craigburke.document.builder.WordDocumentBuilder
import com.craigburke.document.core.builder.DocumentBuilder
import groovy.util.logging.Log4j
import org.hibernate.FetchMode
import org.modelcatalogue.core.CatalogueElement
-import org.modelcatalogue.core.Classification
import org.modelcatalogue.core.DataElement
import org.modelcatalogue.core.EnumeratedType
import org.modelcatalogue.core.Model
+import org.modelcatalogue.core.ModelService
import org.modelcatalogue.core.Relationship
import org.modelcatalogue.core.RelationshipType
import org.modelcatalogue.core.ValueDomain
+import org.modelcatalogue.core.util.docx.ModelCatalogueWordDocumentBuilder
import java.text.SimpleDateFormat
@Log4j
-class ClassificationToDocxExporter {
+class ModelToDocxExporter {
private static final Map HEADER_CELL = [background: '#F2F2F2']
private static final Map HEADER_CELL_TEXT = [font: [color: '#29BDCA', size: 12, bold: true, family: 'Times New Roman']]
@@ -27,15 +27,17 @@ class ClassificationToDocxExporter {
private static final Map DOMAIN_CLASSIFICATION_NAME = [font: [color: '#999999', size: 12, bold: true]]
- final Long classificationId
+ final ModelService modelService
+ final Long modelId
final Set usedValueDomains = new TreeSet([compare: { ValueDomain a, ValueDomain b ->
a?.name <=> b?.name
}] as Comparator)
final Set processedModels = new HashSet()
- ClassificationToDocxExporter(Classification classification) {
- classificationId = classification.getId()
+ ModelToDocxExporter(Model model, ModelService modelService) {
+ this.modelId = model.getId()
+ this.modelService = modelService
}
void export(OutputStream outputStream) {
@@ -43,11 +45,11 @@ class ClassificationToDocxExporter {
usedValueDomains.clear()
processedModels.clear()
- Classification classification = Classification.get(classificationId)
+ Model rootModel = Model.get(modelId)
- log.info "Exporting classification $classification to Word Document"
+ log.info "Exporting model $rootModel to Word Document"
- DocumentBuilder builder = new GelWordDocumentBuilder(outputStream)
+ DocumentBuilder builder = new ModelCatalogueWordDocumentBuilder(outputStream)
def customTemplate = {
'document' font: [family: 'Calibri'], margin: [left: 20, right: 10]
@@ -74,20 +76,20 @@ class ClassificationToDocxExporter {
builder.create {
document(template: customTemplate) {
- paragraph classification.name, style: 'title', align: 'center'
+ paragraph rootModel.name, style: 'title', align: 'center'
paragraph(style: 'subtitle', align: 'center') {
- text "${classification.status}"
+ text "${rootModel.status}"
lineBreak()
text SimpleDateFormat.dateInstance.format(new Date())
}
- if (classification.description) {
+ if (rootModel.description) {
paragraph(style: 'classification.description', margin: [left: 50, right: 50]) {
- text classification.description
+ text rootModel.description
}
}
pageBreak()
- for (Model model in getModelsForClassification(classification.id)) {
+ for (Model model in rootModel.parentOf) {
printModel(builder, model, 1)
}
@@ -99,10 +101,10 @@ class ClassificationToDocxExporter {
log.debug "Exporting value domain $domain to Word Document"
- Map attrs = [ref: "${domain.id}"]
+ Map attrs = [ref: "${domain.id}", style: 'heading2']
attrs.putAll(DOMAIN_NAME)
- heading2 attrs, domain.name
+ paragraph attrs, domain.name
if (domain.classifications) {
paragraph {
@@ -184,12 +186,13 @@ class ClassificationToDocxExporter {
}
}
- log.debug "Classification $classification exported to Word Document"
+ log.debug "Model $rootModel exported to Word Document"
}
private void printModel(DocumentBuilder builder, Model model, int level) {
- if (level == 1 && model.childOf.any { CatalogueElement it -> it.classifications.any { it.id == classificationId } }) {
+ if (level > 3) {
+ // only go 3 levels deep
return
}
@@ -283,14 +286,6 @@ class ClassificationToDocxExporter {
}
}
- if (!(classificationId in model.classifications*.getId())) {
- // do not continue down the tree if the model does not belong to the classification
- processedModels << model.getId()
- return
- }
-
-
-
if (!(model.getId() in processedModels)) {
if (model.countParentOf()) {
for (Model child in model.parentOf) {
@@ -314,19 +309,4 @@ class ClassificationToDocxExporter {
"${relationship.ext['Min Occurs'] ?: 0}..${relationship.ext['Max Occurs'] ?: 'unbounded'}"
}
- private static Collection getModelsForClassification(Long classificationId) {
- def results = Model.createCriteria().list {
- fetchMode "extensions", FetchMode.JOIN
- fetchMode "outgoingRelationships.extensions", FetchMode.JOIN
- fetchMode "outgoingRelationships.destination.classifications", FetchMode.JOIN
- incomingRelationships {
- and {
- eq("relationshipType", RelationshipType.classificationType)
- source { eq('id', classificationId) }
- }
- }
- }
- return results
- }
-
}
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/export/inventory/ModelToXlsxExporter.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/export/inventory/ModelToXlsxExporter.groovy
new file mode 100644
index 0000000000..015763012d
--- /dev/null
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/export/inventory/ModelToXlsxExporter.groovy
@@ -0,0 +1,516 @@
+package org.modelcatalogue.core.export.inventory
+
+import groovy.util.logging.Log4j
+import org.modelcatalogue.builder.spreadsheet.api.Cell
+import org.modelcatalogue.builder.spreadsheet.api.Sheet
+import org.modelcatalogue.builder.spreadsheet.api.SpreadsheetBuilder
+import org.modelcatalogue.builder.spreadsheet.api.Workbook
+import org.modelcatalogue.builder.spreadsheet.poi.PoiSpreadsheetBuilder
+import org.modelcatalogue.core.DataElement
+import org.modelcatalogue.core.DataType
+import org.modelcatalogue.core.EnumeratedType
+import org.modelcatalogue.core.Model
+import org.modelcatalogue.core.ModelService
+
+@Log4j
+class ModelToXlsxExporter {
+
+ final ModelService modelService
+ final Long modelId
+ final Map processedModels = [:]
+
+
+ ModelToXlsxExporter(Model model, ModelService modelService) {
+ this.modelId = model.getId()
+ this.modelService = modelService
+ }
+
+ void export(OutputStream outputStream) {
+ Model model = Model.get(modelId)
+ log.info "Exporting Model ${model.name} (${model.combinedVersion}) to inventory spreadsheet."
+
+ SpreadsheetBuilder builder = new PoiSpreadsheetBuilder()
+ builder.build(outputStream) { Workbook workbook ->
+ apply ModelCatalogueStyles
+ sheet('Models') { Sheet sheet ->
+ buildOutline(sheet, model)
+ }
+
+ int modelsCount = processedModels.size()
+ int counter = 0
+
+ for (Model modelForDetail in processedModels.values()) {
+ log.info "[${++counter}/${modelsCount}] Exporting detail for Model ${modelForDetail.name} (${modelForDetail.combinedVersion})"
+ buildModelDetailSheet(workbook, processedModels, modelForDetail)
+ }
+ }
+
+ log.info "Exported Model ${model.name} (${model.combinedVersion}) to inventory spreadsheet."
+
+ }
+
+ void buildModelDetailSheet(Workbook workbook, Map processedModels, Model model) {
+ workbook.sheet("${model.combinedVersion} ${model.name}") {
+ row {
+ cell {
+ width 10
+ }
+ cell {
+ width 30
+ }
+ cell {
+ width 40
+ }
+ cell {
+ width 10
+ }
+ cell {
+ width 70
+ }
+ cell {
+ width 10
+ }
+ cell {
+ width 70
+ }
+ cell {
+ width 10
+ }
+ cell {
+ width 70
+ }
+ }
+ row {
+ cell {
+ value model.name
+ name getReferenceName(model)
+ style 'h1'
+ colspan 3
+ }
+ }
+ row {
+ if (model.description) {
+ cell {
+ value model.description
+
+ height 100
+
+ style 'description'
+ colspan 3
+ }
+ }
+ }
+
+ row {
+ cell {
+ value 'Classification'
+ style 'property-title'
+ colspan 2
+ }
+ cell {
+ value model.classifications.collect { it.name }.unique().sort().join(', ')
+ style 'property-value'
+ }
+ }
+ for(Model parent in model.childOf) {
+ row {
+ cell {
+ value 'Parent'
+ style 'property-title'
+ colspan 2
+ }
+ cell {
+ value "${parent.name} (${parent.combinedVersion})"
+ style 'property-value'
+ if (parent.getId() in processedModels.keySet()) {
+ link to name getReferenceName(parent)
+ }
+ }
+ }
+ }
+ row {
+ cell {
+ value 'ID'
+ style 'property-title'
+ colspan 2
+ }
+ cell {
+ value model.combinedVersion
+ style 'property-value'
+ }
+ }
+ row {
+ cell {
+ value 'Status'
+ style 'property-title'
+ colspan 2
+ }
+ cell {
+ value model.status
+ style 'property-value'
+ }
+ }
+
+ row()
+
+ row {
+ cell {
+ value 'Last Updated'
+ style 'property-title'
+ colspan 2
+ }
+ cell {
+ value model.lastUpdated
+ style 'date'
+ style 'property-value'
+ }
+ }
+
+ row()
+
+
+ if (model.countContains()) {
+ buildContainedElements(it, model)
+ }
+
+ row()
+
+ row {
+ cell {
+ value '<< Back to all models'
+ link to name 'Models'
+ style 'note'
+ colspan 3
+ }
+ }
+ }
+ }
+
+ private static buildContainedElements(Sheet sheet, Model model) {
+ sheet.with {
+ row {
+ cell {
+ value 'All Contained Data Elements'
+ style 'h2'
+ colspan 3
+ }
+ }
+ row {
+ cell {
+ value 'DE ID'
+ style 'inner-table-header'
+ }
+
+ cell {
+ value 'Data Element Name'
+ style 'inner-table-header'
+ colspan 2
+ }
+ cell {
+ value 'VD ID'
+ style 'inner-table-header'
+ }
+
+ cell {
+ value 'Value Domain Name'
+ style 'inner-table-header'
+ }
+
+ cell {
+ value 'DT ID'
+ style 'inner-table-header'
+ }
+
+ cell {
+ value 'Data Type Name'
+ style 'inner-table-header'
+ }
+
+
+ cell {
+ value 'MU ID'
+ style 'inner-table-header'
+ }
+
+ cell {
+ value 'Measurement Unit Name'
+ style 'inner-table-header'
+ }
+ }
+
+
+ for (DataElement element in model.contains) {
+ row {
+ style 'data-element-row'
+ cell {
+ value element.combinedVersion
+ style {
+ align bottom right
+ }
+ }
+ cell {
+ value element.name
+ colspan 2
+ }
+ if (element.valueDomain) {
+ cell {
+ value element.valueDomain.combinedVersion
+ style {
+ align bottom right
+ }
+ }
+ cell {
+ value element.valueDomain.name
+ }
+ if (element.valueDomain.dataType) {
+ cell {
+ value element.valueDomain.dataType.combinedVersion
+ style {
+ align bottom right
+ }
+ }
+ cell {
+ value element.valueDomain.dataType.name
+ }
+ } else {
+ 2.times { cell() }
+ }
+ if (element.valueDomain.unitOfMeasure) {
+ cell('H') {
+ value element.valueDomain.unitOfMeasure.combinedVersion
+ style {
+ align bottom right
+ }
+ }
+ cell {
+ value element.valueDomain.unitOfMeasure.name
+ }
+ } else {
+ 2.times { cell() }
+ }
+ } else {
+ 6.times { cell() }
+ }
+ }
+ row {
+ style 'data-element-description-row'
+
+
+ cell('B') {
+ value element.description
+ colspan 2
+ }
+
+ if (element.valueDomain) {
+ cell ('E') {
+ value element.valueDomain.description
+ }
+
+ if (element.valueDomain.dataType) {
+ cell ('G') { Cell cell ->
+ createDescriptionAndOrEnums(cell, element.valueDomain.dataType)
+ }
+ }
+ if (element.valueDomain.unitOfMeasure) {
+ cell ('I') {
+ value element.valueDomain.unitOfMeasure.description
+ }
+ }
+
+ }
+
+ }
+ if (element.ext) {
+ for (Map.Entry entry in element.ext) {
+ row {
+ cell('B') {
+ value entry.key
+ style 'metadata-key'
+ }
+ cell {
+ value entry.value
+ style 'metadata-value'
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ static void createDescriptionAndOrEnums(Cell cell, DataType dataType) {
+ if (dataType.description) {
+ cell.text dataType.description
+ }
+
+ if (dataType.instanceOf(EnumeratedType)) {
+ Map enumerations = dataType.enumerations
+
+ if (enumerations) {
+ if (dataType.description) {
+ cell.text '\n\n'
+ }
+
+ cell.text 'Enumerations', {
+ size 12
+ bold
+ }
+
+ cell.text '\n'
+
+ for (Map.Entry entry in enumerations) {
+ cell.text entry.key, {
+ bold
+ }
+ cell.text ': '
+ cell.text entry.value
+ cell.text '\n'
+ }
+ }
+ }
+ }
+
+ private static String getReferenceName(Model model) {
+ "${model.name} (${model.combinedVersion})"
+ }
+
+
+ private void buildOutline(Sheet sheet, Model model) {
+ sheet.with {
+ row(2) {
+ cell {
+ value model.name
+ style 'h1'
+ colspan 2
+ name 'Models'
+ }
+ }
+ row {
+ if (model.description) {
+ cell {
+ value model.description
+
+ height 100
+
+ style 'description'
+ colspan 2
+
+ }
+ }
+ }
+
+ row {
+ cell {
+ value model.classifications.collect { it.name }.unique().sort().join(', ')
+ style 'property-value'
+ style 'model-catalogue-id'
+ colspan 2
+ }
+ }
+
+ row {
+ cell {
+ value model.combinedVersion
+ style 'model-catalogue-id'
+ colspan 2
+ }
+ }
+ row {
+ cell {
+ value model.status
+ style 'status'
+ colspan 2
+ }
+ }
+
+ row()
+
+ row {
+ cell {
+ value new Date()
+ style 'date'
+ colspan 2
+ }
+ }
+
+
+ row()
+
+
+ row {
+ cell {
+ value 'All Contained Models'
+ style 'h2'
+ colspan 2
+ }
+ }
+ row {
+ cell {
+ value 'ID'
+ width 10
+ style 'inner-table-header'
+ }
+
+ cell {
+ value 'Name'
+ width 70
+ style 'inner-table-header'
+ }
+ }
+
+
+ buildChildOutline(it, model, 1)
+
+ row()
+
+ row {
+ cell {
+ value 'Click the model cell to show the detail'
+ style 'note'
+ colspan 2
+ }
+ }
+
+ }
+ }
+
+ private void buildChildOutline(Sheet sheet, Model model, int level) {
+ sheet.row {
+ cell {
+ value model.combinedVersion
+ style {
+ align bottom right
+ }
+ link to name getReferenceName(model)
+ }
+ cell {
+ value model.name
+ link to name getReferenceName(model)
+ style {
+ if (level) {
+ indent (level * 2)
+ }
+ }
+ }
+ }
+
+ if (level > 3) {
+ processedModels.put(model.getId(), model)
+ return
+ }
+
+ if (model.getId() in processedModels.keySet()) {
+ return
+ }
+
+ processedModels.put(model.getId(), model)
+
+ if (model.countParentOf()) {
+ sheet.group {
+ for (Model child in model.parentOf) {
+ buildChildOutline(sheet, child, level + 1)
+ }
+ }
+ }
+ }
+}
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/publishing/changelog/ChangelogGenerator.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/publishing/changelog/ChangelogGenerator.groovy
new file mode 100644
index 0000000000..d54aac879e
--- /dev/null
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/publishing/changelog/ChangelogGenerator.groovy
@@ -0,0 +1,524 @@
+package org.modelcatalogue.core.publishing.changelog
+
+import com.craigburke.document.core.builder.DocumentBuilder
+import com.google.common.collect.LinkedHashMultimap
+import com.google.common.collect.Multimap
+import grails.util.GrailsNameUtils
+import grails.util.Holders
+import groovy.util.logging.Log4j
+import org.codehaus.groovy.grails.commons.GrailsDomainClass
+import org.hibernate.proxy.HibernateProxyHelper
+import org.modelcatalogue.core.*
+import org.modelcatalogue.core.audit.AuditService
+import org.modelcatalogue.core.audit.Change
+import org.modelcatalogue.core.audit.ChangeType
+import org.modelcatalogue.core.audit.DefaultAuditor
+import org.modelcatalogue.core.comments.Comment
+import org.modelcatalogue.core.comments.CommentsService
+import org.modelcatalogue.core.util.delayable.Delayable
+import org.modelcatalogue.core.util.docx.ModelCatalogueWordDocumentBuilder
+
+import java.text.SimpleDateFormat
+
+@Log4j
+class ChangelogGenerator {
+
+ private final AuditService auditService
+ private final ModelService modelService
+ private final CommentsService commentsService
+
+ private final Map> commentsCache = [:]
+
+ ChangelogGenerator(AuditService auditService, ModelService modelService) {
+ this.auditService = auditService
+ this.modelService = modelService
+ try {
+ commentsService = Holders.applicationContext.getBean(CommentsService)
+ } catch (Exception ignored) {
+ commentsService = null
+ log.info "Comments are not enabled for this catalogue."
+ }
+ }
+
+ void generateChangelog(Model model, OutputStream outputStream) {
+ log.info "Generating changelog for model $model.name ($model.combinedVersion)"
+ DocumentBuilder builder = new ModelCatalogueWordDocumentBuilder(outputStream)
+
+ def customTemplate = {
+ 'document' font: [family: 'Calibri'], margin: [left: 30, right: 30]
+ 'paragraph.title' font: [color: '#13D4CA', size: 26.pt], margin: [top: 200.pt]
+ 'paragraph.subtitle' font: [color: '#13D4CA', size: 18.pt]
+ 'paragraph.description' font: [color: '#13D4CA', size: 16.pt, italic: true], margin: [left: 30, right: 30]
+ 'heading1' font: [size: 20, bold: true]
+ 'heading2' font: [size: 18, bold: true]
+ 'heading3' font: [size: 16, bold: true, italic: true]
+ 'heading4' font: [size: 14, italic: true]
+ 'heading5' font: [size: 13]
+ 'heading6' font: [size: 12, bold: true]
+ 'paragraph.heading1' font: [size: 20, bold: true]
+ 'paragraph.heading2' font: [size: 18, bold: true]
+ 'paragraph.heading3' font: [size: 16, bold: true]
+ 'paragraph.heading4' font: [size: 16]
+ 'paragraph.heading5' font: [size: 15]
+ 'paragraph.heading6' font: [size: 14]
+ 'cell.headerCell' font: [color: '#29BDCA', size: 12.pt, bold: true], background: '#F2F2F2'
+ 'cell' font: [size: 10.pt]
+
+ }
+
+ Delayable delayable = new Delayable<>(builder)
+
+ builder.create {
+ document(template: customTemplate) {
+ paragraph "Changelog for ${model.name}", style: 'title', align: 'center'
+ paragraph(style: 'subtitle', align: 'center') {
+ text "${model.combinedVersion}"
+ lineBreak()
+ text "${model.status}"
+ lineBreak()
+ text SimpleDateFormat.dateInstance.format(new Date())
+ }
+ if (model.description) {
+ paragraph(style: 'description', margin: [left: 50, right: 50]) {
+ text model.description
+ }
+ }
+ pageBreak()
+
+ delayable.whilePaused {
+ delayable.heading1 "Root Model Changes"
+ printPropertiesChanges(delayable, model)
+ }
+
+ heading1 'Models'
+
+ Collection models = getModelsForRootModel(model)
+ int counter = 1
+ int size = models.size()
+ for (Model child in models) {
+ log.info "[${counter++}/${size}] Processing changes from Model $child.name"
+ delayable.whilePaused {
+ printPropertiesChanges(delayable, child)
+ printModelStructuralChanges(delayable, child)
+ }
+
+ }
+ }
+ }
+
+ log.info "Model $model.name changelog exported to Word Document"
+
+
+ }
+
+ private String getUpdateText(CatalogueElement element) {
+ if (getChanges(element, ChangeType.NEW_VERSION_CREATED)) {
+ return "New version ${element.versionNumber} of the ${GrailsNameUtils.getNaturalName(element.class.name)}"
+ }
+
+ if (getChanges(element, ChangeType.NEW_ELEMENT_CREATED)) {
+ return "New ${GrailsNameUtils.getNaturalName(element.class.name)}"
+ }
+
+ if (getChanges(element, ChangeType.ELEMENT_DEPRECATED)) {
+ return "${GrailsNameUtils.getNaturalName(element.class.name)} has been deprecated "
+ }
+
+ List changedProperties = getChanges(element, ChangeType.PROPERTY_CHANGED, ChangeType.METADATA_CREATED, ChangeType.METADATA_DELETED, ChangeType.METADATA_UPDATED)
+
+ if (changedProperties) {
+ Set changedPropertiesLabels = new TreeSet()
+
+ for (Change change in changedProperties) {
+ String propLabel = change.property
+ if (change.type == ChangeType.PROPERTY_CHANGED) {
+ if (change.property == 'enumAsString') {
+ propLabel = 'Enumerations'
+ } else {
+ propLabel = GrailsNameUtils.getNaturalName(change.property)
+ }
+ }
+ changedPropertiesLabels << propLabel
+ }
+
+ return "${changedProperties.join(', ')} ${changedProperties.size() > 1 ? 'have' : 'has'} been changed"
+ }
+
+ GrailsDomainClass grailsDomainClass = Holders.grailsApplication.getDomainClass(HibernateProxyHelper.getClassWithoutInitializingProxy(element).name)
+
+ grailsDomainClass.persistentProperties.findAll { it.oneToOne || it.manyToOne }.sort { it.name }.each {
+ def value = element.getProperty(it.name)
+ if (value.respondsTo('instanceOf') && value.instanceOf(CatalogueElement)) {
+ String change = getUpdateText(value as CatalogueElement)
+ if (change) {
+ return "${GrailsNameUtils.getNaturalName(it.name)} > ${change}"
+ }
+ }
+ }
+ return null
+ }
+
+ private void printPropertiesChanges(Delayable builder, CatalogueElement element, int headingLevel = 2) {
+ builder.with {
+ "heading${Math.min(headingLevel, 5)}" "$element.name ($element.combinedVersion, $element.status)", ref: "${element.getId()}"
+
+ if (getChanges(element, ChangeType.NEW_VERSION_CREATED)) {
+ requestRun()
+ paragraph {
+ text "This is a new version "
+ text element.versionNumber, font: [bold: true]
+ text " of the ${GrailsNameUtils.getNaturalName(element.class.name)}."
+ }
+ } else if (getChanges(element, ChangeType.NEW_ELEMENT_CREATED)) {
+ requestRun()
+ paragraph "This is a new ${GrailsNameUtils.getNaturalName(element.class.name)}"
+ } else if (getChanges(element, ChangeType.ELEMENT_DEPRECATED)) {
+ requestRun()
+ paragraph {
+ text "This ${GrailsNameUtils.getNaturalName(element.class.name)} has been "
+ text " deprecated", font: [bold: true]
+ }
+ }
+
+
+ if (commentsService?.forumEnabled) {
+ builder.whilePaused {
+ builder.heading3 'Comments'
+
+ List comments = commentsCache[element.getId()]
+
+ if (comments == null) {
+ comments = commentsService.getComments(element)
+ commentsCache[element.getId()] = comments
+ }
+
+ if (comments) {
+ // first comment is always a description and link
+ for (Comment comment in comments.tail()) {
+ builder.requestRun()
+ builder.with {
+ paragraph "${comment.username} (${SimpleDateFormat.dateTimeInstance.format(comment.created)})" , font: [bold: true]
+ paragraph comment.text
+ }
+ }
+ }
+ }
+ }
+
+
+ List changedProperties = getChanges(element, ChangeType.PROPERTY_CHANGED, ChangeType.METADATA_CREATED, ChangeType.METADATA_DELETED, ChangeType.METADATA_UPDATED)
+
+ if (changedProperties) {
+ requestRun()
+ Map> rows = new TreeMap>().withDefault {['', '']}
+
+ for(Change change in changedProperties) {
+ String propLabel = change.property
+ if (change.type == ChangeType.PROPERTY_CHANGED) {
+ if (change.property == 'enumAsString') {
+ propLabel = 'Enumerations'
+ } else {
+ propLabel = GrailsNameUtils.getNaturalName(change.property)
+ }
+ }
+ propLabel = propLabel ?: ''
+ List vals = rows[propLabel]
+ vals[0] = vals[0] ?: valueForPrint(change.property, change.oldValue)
+ vals[1] = valueForPrint(change.property, change.newValue)
+ rows[propLabel] = vals
+ }
+
+ paragraph font: [bold: true], "Changed Properties"
+
+ printChangesTable builder, rows
+ }
+
+ GrailsDomainClass grailsDomainClass = Holders.grailsApplication.getDomainClass(HibernateProxyHelper.getClassWithoutInitializingProxy(element).name)
+
+ grailsDomainClass.persistentProperties.findAll { it.oneToOne || it.manyToOne }.sort{ it.name }.each {
+ def value = element.getProperty(it.name)
+ if (value.respondsTo('instanceOf') && value.instanceOf(CatalogueElement)) {
+ builder.whilePaused {
+ printPropertiesChanges(builder, value as CatalogueElement, headingLevel + 1)
+ }
+ }
+ }
+
+
+ }
+ }
+
+ private void printChangesTable(Delayable builder, Map> rows) {
+ builder.table(border: [size: 1, color: '#D2D2D2'], columns: [1,2,2], font: [size: 10]) {
+ row(background: '#F2F2F2') {
+ cell "Property", style: 'headerCell'
+ cell "Old Value", style: 'headerCell'
+ cell "New Value", style: 'headerCell'
+ }
+
+ for (Map.Entry> change in rows) {
+ String background = "#FFFFFF"
+
+ if (change.value[0] && !change.value[1]) {
+ background = "#F2DEDE"
+ } else if (!change.value[0] && change.value[1]) {
+ background = '#DFF0D8'
+ }
+ row (background: background){
+ cell change.key, font: [bold: true]
+ cell change.value[0]
+ cell change.value[1]
+ }
+ }
+ }
+ }
+
+ private void printModelStructuralChanges(Delayable builder, Model model) {
+ List relationshipChanges = getChanges(model, ChangeType.RELATIONSHIP_CREATED, ChangeType.RELATIONSHIP_DELETED, ChangeType.RELATIONSHIP_ARCHIVED, ChangeType.RELATIONSHIP_METADATA_CREATED, ChangeType.RELATIONSHIP_METADATA_DELETED, ChangeType.RELATIONSHIP_METADATA_UPDATED)
+
+ Multimap byDestinationsAndSources = LinkedHashMultimap.create()
+
+ for (Change ch in relationshipChanges) {
+ byDestinationsAndSources.put "out:${getRelationshipType(ch)}:${getDestinationId(ch)}".toString(), ch
+ byDestinationsAndSources.put "in:${getRelationshipType(ch)}:${getSourceId(ch)}".toString(), ch
+ }
+
+ handleRelationshipChanges(builder, byDestinationsAndSources, RelationshipChangesCheckConfiguration.create(model, RelationshipType.hierarchyType).withChangesSummaryHeading("Changed Child Models").withNewRelationshipNote("New child model").withRemovedRelationshipNote("Child model removed"))
+ handleRelationshipChanges(builder, byDestinationsAndSources, RelationshipChangesCheckConfiguration.create(model, RelationshipType.containmentType).withChangesSummaryHeading("Changed Data Elements").withNewRelationshipNote("New data element").withRemovedRelationshipNote("Data element removed").withDeep(true))
+ handleRelationshipChanges(builder, byDestinationsAndSources, RelationshipChangesCheckConfiguration.create(model, RelationshipType.synonymType).withChangesSummaryHeading("Changed Synonyms").withNewRelationshipNote("New synonym").withRemovedRelationshipNote("Synonym removed"))
+ handleRelationshipChanges(builder, byDestinationsAndSources, RelationshipChangesCheckConfiguration.create(model, RelationshipType.relatedToType).withChangesSummaryHeading("Changed Relations").withNewRelationshipNote("Newly related").withRemovedRelationshipNote("No longer related"))
+ handleRelationshipChanges(builder, byDestinationsAndSources, RelationshipChangesCheckConfiguration.create(model, RelationshipType.baseType).withChangesSummaryHeading("Changed Bases").withNewRelationshipNote("Newly based on").withRemovedRelationshipNote("No longer based on").withIncoming(true))
+ }
+
+ private void handleRelationshipChanges(Delayable builder, Multimap byDestinationsAndSources, RelationshipChangesCheckConfiguration configuration) {
+ builder.whilePaused {
+ builder.heading3 configuration.changesSummaryHeading
+
+ Map titleRows = new TreeMap()
+ Map>> metadataRows = new TreeMap>>()
+
+ builder.whilePaused {
+ for (CatalogueElement element in (configuration.incoming ? configuration.element.getIncomingRelationsByType(configuration.type) : configuration.element.getOutgoingRelationsByType(configuration.type))) {
+ String heading = "$element.name ($element.combinedVersion, $element.status)"
+
+ Set changes = byDestinationsAndSources.removeAll("${configuration.incoming ? 'in' : 'out'}:${configuration.type.name}:${element.getLatestVersionId() ?: element.getId()}".toString())
+ if (changes) {
+ builder.requestRun()
+
+ if (changes.any { it.type == ChangeType.RELATIONSHIP_CREATED }) {
+ titleRows[heading] = configuration.newRelationshipNote
+ }
+ Set metadataChanges = changes.findAll {
+ it.type in [ChangeType.RELATIONSHIP_METADATA_CREATED, ChangeType.RELATIONSHIP_METADATA_DELETED, ChangeType.RELATIONSHIP_METADATA_UPDATED]
+ }
+ if (metadataChanges) {
+
+ Map> rows = new TreeMap>().withDefault {
+ ['', '']
+ }
+
+ for (Change change in metadataChanges) {
+ String propName = getRelationshipMetadataName(change) ?: ''
+ List vals = rows[propName]
+ vals[0] = (getOldRelationshipMetadataValue(change)?.toString() ?: '')
+ vals[1] = (getNewRelationshipMetadataValue(change)?.toString() ?: '')
+ rows[propName] = vals
+ }
+
+ metadataRows[heading] = rows
+
+ }
+ } else if (configuration.deep) {
+ String update = getUpdateText(element)
+ if (update) {
+ titleRows[heading] = "$update\n (See following)"
+ }
+ }
+ }
+
+
+ Set otherHierarchyChanges = byDestinationsAndSources.keySet().findAll { it.startsWith("${configuration.incoming ? 'in' : 'out'}:${configuration.type.name}:") }
+
+ for (String key in otherHierarchyChanges) {
+ Set rest = byDestinationsAndSources.removeAll(key)
+ Change deleteChange = new ArrayList(rest).reverse().find { it.type == ChangeType.RELATIONSHIP_DELETED}
+
+ if (!deleteChange) {
+ continue
+ }
+
+ builder.requestRun()
+
+ def value = DefaultAuditor.readValue(deleteChange.oldValue)
+
+ String heading = "${value.destination.name} (${value.destination.latestVersionId}.${value.destination.versionNumber}, $value.destination.status)"
+
+ if (configuration.incoming) {
+ heading = "${value.source.name} (${value.source.latestVersionId}.${value.source.versionNumber}, $value.source.status)"
+ }
+
+ titleRows[heading] = configuration.removedRelationshipNote
+ }
+
+ builder.table(border: [size: 1, color: '#D2D2D2'], columns: [1] * 10, font: [size: 10]) {
+ for (Map.Entry entry in titleRows) {
+ Map> metadataChanges = metadataRows[entry.key]
+ if (entry.value || metadataChanges) {
+ String background = "#FFFFFF"
+
+ if (entry.value == configuration.newRelationshipNote) {
+ background = "#DFF0D8"
+ } else if (entry.value == configuration.removedRelationshipNote) {
+ background = '#F2DEDE'
+ }
+ row(background: background) {
+ cell entry.key, colspan: 5, font: [bold: true, size: 12]
+ cell(entry.value ?: 'Metadata Updated', colspan: 5)
+ }
+ }
+ if (metadataChanges) {
+ row(background: '#F2F2F2') {
+ cell 'Updated Metadata', colspan: 2,style: 'headerCell', font: [size: 10]
+ cell 'Old Value', colspan: 4, style: 'headerCell'
+ cell 'New Value', colspan: 4, style: 'headerCell'
+ }
+ for (Map.Entry> metadataEntry in metadataChanges) {
+ String background = "#FFFFFF"
+
+ if (metadataEntry.value[0] && !metadataEntry.value[1]) {
+ background = "#F2DEDE"
+ } else if (!metadataEntry.value[0] && metadataEntry.value[1]) {
+ background = '#DFF0D8'
+ }
+ row(background: background) {
+ cell metadataEntry.key, colspan: 2
+ cell metadataEntry.value[0], colspan: 4
+ cell metadataEntry.value[1], colspan: 4
+ }
+ }
+ }
+ }
+ }
+ }
+
+
+ if (configuration.deep) {
+ for (CatalogueElement element in (configuration.incoming ? configuration.element.getIncomingRelationsByType(configuration.type) : configuration.element.getOutgoingRelationsByType(configuration.type))) {
+ builder.whilePaused {
+ printPropertiesChanges(builder, element, 4)
+ }
+ }
+ }
+ }
+ }
+
+ private static String getRelationshipMetadataName(Change ch) {
+ switch (ch.type) {
+ case [ChangeType.RELATIONSHIP_METADATA_DELETED, ChangeType.RELATIONSHIP_METADATA_UPDATED]:
+ def value = DefaultAuditor.readValue(ch.oldValue)
+ return value instanceof CharSequence ? value : value?.name
+ case ChangeType.RELATIONSHIP_METADATA_CREATED:
+ def value = DefaultAuditor.readValue(ch.newValue)
+ return value instanceof CharSequence ? value : value?.name
+
+ default:
+ throw new IllegalArgumentException("Cannot get old relationship metadata value from $ch")
+ }
+ }
+
+ private static String getOldRelationshipMetadataValue(Change ch) {
+ switch (ch.type) {
+ case [ChangeType.RELATIONSHIP_METADATA_DELETED, ChangeType.RELATIONSHIP_METADATA_UPDATED]:
+ def value = org.modelcatalogue.core.audit.DefaultAuditor.readValue(ch.oldValue)
+ return value instanceof CharSequence ? value : value?.extensionValue
+ case ChangeType.RELATIONSHIP_METADATA_CREATED:
+ return ''
+
+ default:
+ throw new IllegalArgumentException("Cannot get old relationship metadata value from $ch")
+ }
+ }
+
+ private static String getNewRelationshipMetadataValue(Change ch) {
+ switch (ch.type) {
+ case [ChangeType.RELATIONSHIP_METADATA_CREATED, ChangeType.RELATIONSHIP_METADATA_UPDATED]:
+ def value = org.modelcatalogue.core.audit.DefaultAuditor.readValue(ch.newValue)
+ return value instanceof CharSequence ? value : value?.extensionValue
+ case ChangeType.RELATIONSHIP_METADATA_DELETED:
+ return ''
+
+ default:
+ throw new IllegalArgumentException("Cannot get new relationship metadata value from $ch")
+ }
+ }
+
+ private static Object getRelationship(Change ch) {
+ switch (ch.type) {
+ case [ChangeType.RELATIONSHIP_CREATED, ChangeType.RELATIONSHIP_ARCHIVED]:
+ return DefaultAuditor.readValue(ch.newValue)
+ case ChangeType.RELATIONSHIP_DELETED:
+ return DefaultAuditor.readValue(ch.oldValue)
+ case [ChangeType.RELATIONSHIP_METADATA_CREATED, ChangeType.RELATIONSHIP_METADATA_UPDATED]:
+ return DefaultAuditor.readValue(ch.newValue).relationship
+ case ChangeType.RELATIONSHIP_METADATA_DELETED:
+ return DefaultAuditor.readValue(ch.oldValue).relationship
+
+ default:
+ throw new IllegalArgumentException("Cannot get relationship type from $ch")
+ }
+ }
+
+ private static String getRelationshipType(Change ch) {
+ getRelationship(ch)?.type?.name
+ }
+
+ private static Long getDestinationId(Change ch) {
+ def rel = getRelationship(ch)
+ if (rel.destination.latestVersionId) {
+ return rel.destination.latestVersionId
+ }
+ return rel.destination.id
+ }
+ private static Long getSourceId(Change ch) {
+ def rel = getRelationship(ch)
+ if (rel.source.latestVersionId) {
+ return rel.source.latestVersionId
+ }
+ return rel.source.id
+ }
+
+ private List getChanges(CatalogueElement element, ChangeType... types) {
+ auditService.getChanges(element, sort: 'dateCreated', order: 'asc'){
+ ne 'undone', true
+ isNull 'parentId'
+ if (types) {
+ inList 'type', types.toList()
+ }
+ }.items
+ }
+
+ private Collection getModelsForRootModel(Model model) {
+ modelService.getSubModels(model).items
+ }
+
+ private static String valueForPrint(String propertyName, String storedValue) {
+ if (!storedValue) {
+ return ''
+ }
+ def value = DefaultAuditor.readValue(storedValue)
+ if (!value) {
+ return ''
+ }
+ if (value instanceof CharSequence) {
+ if (propertyName == 'enumAsString') {
+ return EnumeratedType.stringToMap(value?.toString()).collect { "$it.key: $it.value" }.join('\n')
+ }
+ }
+ if (value instanceof CatalogueElement) {
+ return value.name
+ }
+ return value.toString()
+ }
+
+
+}
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/publishing/changelog/RelationshipChangesCheckConfiguration.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/publishing/changelog/RelationshipChangesCheckConfiguration.groovy
new file mode 100644
index 0000000000..9abdbbbc79
--- /dev/null
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/publishing/changelog/RelationshipChangesCheckConfiguration.groovy
@@ -0,0 +1,56 @@
+package org.modelcatalogue.core.publishing.changelog
+
+import groovy.transform.PackageScope
+import org.modelcatalogue.core.CatalogueElement
+import org.modelcatalogue.core.RelationshipType
+
+@PackageScope class RelationshipChangesCheckConfiguration {
+ final CatalogueElement element
+ final RelationshipType type
+
+ boolean incoming = false
+ boolean deep
+
+ String changesSummaryHeading
+ String newRelationshipNote
+ String removedRelationshipNote
+
+ static RelationshipChangesCheckConfiguration create(CatalogueElement element, RelationshipType type) {
+ return new RelationshipChangesCheckConfiguration(element, type)
+ }
+
+ private RelationshipChangesCheckConfiguration(CatalogueElement element, RelationshipType type) {
+ this.element = element
+ this.type = type
+ }
+
+ RelationshipChangesCheckConfiguration withChangesSummaryHeading(String text) {
+ this.changesSummaryHeading = text
+ this
+ }
+
+ RelationshipChangesCheckConfiguration withNewRelationshipNote(String text) {
+ this.newRelationshipNote = text
+ this
+ }
+
+ RelationshipChangesCheckConfiguration withRemovedRelationshipNote(String text) {
+ this.removedRelationshipNote = text
+ this
+ }
+
+ RelationshipChangesCheckConfiguration withDeep(boolean deep) {
+ this.deep = deep
+ this
+ }
+
+ RelationshipChangesCheckConfiguration withIncoming(boolean incoming) {
+ this.incoming = incoming
+ this
+ }
+
+
+
+
+
+}
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/Inheritance.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/Inheritance.groovy
new file mode 100644
index 0000000000..db9f72c9ec
--- /dev/null
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/Inheritance.groovy
@@ -0,0 +1,43 @@
+package org.modelcatalogue.core.util
+
+import org.modelcatalogue.core.RelationshipType
+import org.modelcatalogue.core.CatalogueElement
+import org.modelcatalogue.core.Relationship
+
+class Inheritance {
+
+ static void withAllChildren(CatalogueElement element, Set processed = new HashSet([element]), @DelegatesTo(CatalogueElement) Closure closure) {
+ for (Relationship relationship in new ArrayList(element.getOutgoingRelationshipsByType(RelationshipType.baseType))) {
+ if (relationship.destination in processed) {
+ continue
+ }
+ processed << relationship.destination
+ relationship.destination.with closure
+ withAllChildren(relationship.destination, processed, closure)
+ }
+ }
+
+ static void withAllParents(CatalogueElement element, Set processed = new HashSet([element]), @DelegatesTo(CatalogueElement) Closure closure) {
+ for (Relationship relationship in new ArrayList(element.getIncomingRelationshipsByType(RelationshipType.baseType))) {
+ if (relationship.source in processed) {
+ continue
+ }
+ processed << relationship.source
+ relationship.source.with closure
+ withAllParents(relationship.source, processed, closure)
+ }
+ }
+
+ static void withChildren(CatalogueElement element, @DelegatesTo(CatalogueElement) Closure closure) {
+ for (Relationship relationship in new ArrayList(element.getOutgoingRelationshipsByType(RelationshipType.baseType))) {
+ relationship.destination.with closure
+ }
+ }
+
+ static void withParents(CatalogueElement element, @DelegatesTo(CatalogueElement) Closure closure) {
+ for (Relationship relationship in new ArrayList(element.getIncomingRelationshipsByType(RelationshipType.baseType))) {
+ relationship.source.with closure
+ }
+ }
+
+}
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/builder/DefaultRelationshipConfiguration.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/builder/DefaultRelationshipConfiguration.groovy
index 652b3d5954..cedd969213 100644
--- a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/builder/DefaultRelationshipConfiguration.groovy
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/builder/DefaultRelationshipConfiguration.groovy
@@ -6,11 +6,16 @@ class DefaultRelationshipConfiguration implements RelationshipConfiguration {
Map extensions = [:]
boolean archived = false
+ boolean inherited = true
void archived(boolean archived) {
this.archived = archived
}
+ void inherited(boolean inherited) {
+ this.inherited = inherited
+ }
+
void ext(String key, String value) {
extensions[key] = value
}
@@ -19,3 +24,4 @@ class DefaultRelationshipConfiguration implements RelationshipConfiguration {
extensions.putAll(values)
}
}
+
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/delayable/Delayable.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/delayable/Delayable.groovy
new file mode 100644
index 0000000000..a2be4dda18
--- /dev/null
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/delayable/Delayable.groovy
@@ -0,0 +1,153 @@
+package org.modelcatalogue.core.util.delayable
+
+import groovy.transform.stc.ClosureParams
+import groovy.transform.stc.FromString
+import groovy.util.logging.Log4j
+
+/**
+ * Object wrapper with ability to delay and rollback method calls.
+ *
+ * This class is designed to be used with various builders where delayed method does not return any value.
+ *
+ * @param < T > the type of the delegate
+ */
+@Log4j
+class Delayable {
+
+ private final T delegate
+ private final List> queues = [].withDefault { [] }
+ private final List runRequests = [].withDefault { Boolean.FALSE }
+ private final List runInProgress = [].withDefault { Boolean.FALSE }
+
+ private int pauseLevel = -1
+
+ Delayable(T delegate) {
+ this.delegate = delegate
+ }
+
+ /**
+ * All method call will from this call return null and will be queued for later execution using the #run() method.
+ */
+ protected void pauseAndRecord() {
+ pauseLevel++
+ }
+
+ /**
+ * @return true
if the execution is currently paused
+ */
+ protected boolean isPaused() {
+ pauseLevel >= 0
+ }
+
+ /**
+ * @return true
if the run is requested
+ */
+ protected boolean isRunRequested() {
+ runRequests[pauseLevel]
+ }
+
+ /**
+ * Resets the waiting queue so none of the queued method is invoked on the delegate regardless the run requested state.
+ *
+ * Do not call this method from within #whilePaused(boolean, Closure) method.
+ *
+ * @return number of methods queued
+ */
+ protected int resetAndUnpause(boolean ignoreNotPaused = false) {
+ if (!paused && !ignoreNotPaused) {
+ throw new IllegalStateException("Execution is not paused. Run pauseAndRecord() before to start queuing method calls.")
+ }
+ List queue = queues[pauseLevel]
+ int ret = queue.size()
+ queue.clear()
+ runRequests[pauseLevel] = Boolean.FALSE
+ pauseLevel--
+ return ret
+ }
+
+ /**
+ * Resets the waiting queue so none of the queued method is invoked on the delegate regardless the run requested state
+ * but does not unpause the execution.
+ * @return number of methods queued
+ */
+ protected int reset(boolean ignoreNotPaused = false) {
+ if (!paused && !ignoreNotPaused) {
+ throw new IllegalStateException("Execution is not paused. Run pauseAndRecord() before to start queuing method calls.")
+ }
+ List queue = queues[pauseLevel]
+ int ret = queue.size()
+ queue.clear()
+ return ret
+ }
+
+ /**
+ * Invokes all the queued methods on the delegate regardless the run requested state.
+ */
+ protected void run() {
+ List queue = queues[pauseLevel]
+ if (pauseLevel > 0 && !runInProgress[pauseLevel - 1]) {
+ queues[pauseLevel - 1].addAll(queue)
+ } else {
+ runInProgress[pauseLevel] = Boolean.TRUE
+ for (DelayableQueueItem item in queue) {
+ delegate.invokeMethod(item.methodName, item.args)
+ }
+ runInProgress[pauseLevel] = Boolean.FALSE
+ }
+ queue.clear()
+ runRequests[pauseLevel] = Boolean.FALSE
+ pauseLevel--
+ }
+
+ void requestRun() {
+ for (int i in 0..pauseLevel) {
+ runRequests[i] = Boolean.TRUE
+ }
+ }
+
+ /**
+ * Runs only if the run was requested from the last call to the #pauseAndRecord().
+ */
+ protected void runIfRequested() {
+ if (runRequested) {
+ run()
+ } else {
+ resetAndUnpause(true)
+ }
+ }
+
+ /**
+ * Invokes the method on the delegate if not paused or if the name does not match any factory or queues the method
+ * for later execution with the #run() method.
+ *
+ * While paused, you can remove all the queued methods later using #resetAndUnpause().
+ *
+ * @param name name of the method to be invoked or queued
+ * @param args arguments of the method to be invoked or queued
+ * @return null
if this object is in paused state or the result of method invocation otherwise.
+ */
+ @Override
+ Object invokeMethod(String name, Object args) {
+ if (paused && !runInProgress[pauseLevel]) {
+ queues[pauseLevel] << new DelayableQueueItem(name, args)
+ return null
+ }
+ delegate.invokeMethod(name, args)
+ }
+
+
+ void whilePaused(@ClosureParams(value = FromString, options = 'Delayable') Closure closure) {
+ pauseAndRecord()
+ try {
+ closure this
+ } catch(Exception e) {
+ reset(true)
+ return
+ }
+ runIfRequested()
+ }
+
+ // TODO: publish more API
+
+
+}
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/delayable/DelayableQueueItem.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/delayable/DelayableQueueItem.groovy
new file mode 100644
index 0000000000..2f72db40d8
--- /dev/null
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/delayable/DelayableQueueItem.groovy
@@ -0,0 +1,13 @@
+package org.modelcatalogue.core.util.delayable
+
+import groovy.transform.PackageScope
+
+@PackageScope class DelayableQueueItem {
+ final String methodName
+ final Object[] args
+
+ DelayableQueueItem(String methodName, Object[] args) {
+ this.methodName = methodName
+ this.args = args
+ }
+}
diff --git a/ModelCatalogueGenomicsPlugin/src/groovy/org/modelcatalogue/core/gel/GelWordDocumentBuilder.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/docx/ModelCatalogueWordDocumentBuilder.groovy
similarity index 95%
rename from ModelCatalogueGenomicsPlugin/src/groovy/org/modelcatalogue/core/gel/GelWordDocumentBuilder.groovy
rename to ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/docx/ModelCatalogueWordDocumentBuilder.groovy
index 35bf02e24c..6e013f7a28 100644
--- a/ModelCatalogueGenomicsPlugin/src/groovy/org/modelcatalogue/core/gel/GelWordDocumentBuilder.groovy
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/docx/ModelCatalogueWordDocumentBuilder.groovy
@@ -1,11 +1,11 @@
-package org.modelcatalogue.core.gel
+package org.modelcatalogue.core.util.docx
import com.craigburke.document.builder.BasicDocumentPartTypes
import com.craigburke.document.builder.WordDocumentBuilder
-class GelWordDocumentBuilder extends WordDocumentBuilder {
+class ModelCatalogueWordDocumentBuilder extends WordDocumentBuilder {
- GelWordDocumentBuilder(OutputStream outputStream) {
+ ModelCatalogueWordDocumentBuilder(OutputStream outputStream) {
super(outputStream)
}
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/marshalling/RelationshipMarshallers.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/marshalling/RelationshipMarshallers.groovy
index c24f772e00..35f27b0325 100644
--- a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/marshalling/RelationshipMarshallers.groovy
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/marshalling/RelationshipMarshallers.groovy
@@ -24,6 +24,7 @@ class RelationshipMarshallers extends AbstractMarshaller {
destination: CatalogueElementMarshaller.minimalCatalogueElementJSON(rel.destination),
type: rel.relationshipType.info,
archived: rel.archived,
+ inherited: rel.inherited,
ext: OrderedMap.toJsonMap(rel.ext),
elementType: Relationship.name,
classification: CatalogueElementMarshaller.minimalCatalogueElementJSON(rel.classification)
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/marshalling/RelationshipsMarshaller.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/marshalling/RelationshipsMarshaller.groovy
index c70eaface7..7f23ccfb34 100644
--- a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/marshalling/RelationshipsMarshaller.groovy
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/marshalling/RelationshipsMarshaller.groovy
@@ -16,7 +16,7 @@ class RelationshipsMarshaller extends ListWrapperMarshaller {
ret.type = relationsList.type
ret.direction = relationsList.direction.actionName
ret.list = relationsList.items.collect {
- [id: it.id, type: it.relationshipType, ext: OrderedMap.toJsonMap(it.ext), element: CatalogueElementMarshaller.minimalCatalogueElementJSON(relationsList.direction.getElement(relationsList.owner, it)), relation: relationsList.direction.getRelation(relationsList.owner, it), direction: relationsList.direction.getDirection(relationsList.owner, it), removeLink: getDeleteLink(relationsList.owner, it), archived: it.archived, elementType: Relationship.name, classification: CatalogueElementMarshaller.minimalCatalogueElementJSON(it.classification)]
+ [id: it.id, type: it.relationshipType, ext: OrderedMap.toJsonMap(it.ext), element: CatalogueElementMarshaller.minimalCatalogueElementJSON(relationsList.direction.getElement(relationsList.owner, it)), relation: relationsList.direction.getRelation(relationsList.owner, it), direction: relationsList.direction.getDirection(relationsList.owner, it), removeLink: getDeleteLink(relationsList.owner, it), archived: it.archived, inherited: it.inherited, elementType: Relationship.name, classification: CatalogueElementMarshaller.minimalCatalogueElementJSON(it.classification)]
}
ret
}
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/test/FileOpener.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/test/FileOpener.groovy
new file mode 100644
index 0000000000..6ad4b4329a
--- /dev/null
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/test/FileOpener.groovy
@@ -0,0 +1,24 @@
+package org.modelcatalogue.core.util.test
+
+import java.awt.Desktop
+
+class FileOpener {
+
+
+ /**
+ * Tries to open the file in Word. Only works locally on Mac at the moment. Ignored otherwise.
+ * Main purpose of this method is to quickly open the generated file for manual review.
+ * @param file file to be opened
+ */
+ static void open(File file) {
+ try {
+ if (Desktop.desktopSupported && Desktop.desktop.isSupported(Desktop.Action.OPEN)) {
+ Desktop.desktop.open(file)
+ println file
+ Thread.sleep(60000)
+ }
+ } catch(ignored) {
+ ignored.printStackTrace()
+ }
+ }
+}
diff --git a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/test/TestDataHelper.groovy b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/test/TestDataHelper.groovy
index 113771d5d0..b7cc44ea6e 100644
--- a/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/test/TestDataHelper.groovy
+++ b/ModelCatalogueCorePlugin/src/groovy/org/modelcatalogue/core/util/test/TestDataHelper.groovy
@@ -1,11 +1,9 @@
package org.modelcatalogue.core.util.test
+import grails.util.Metadata
import groovy.sql.Sql
import org.hibernate.SessionFactory
-/**
- * Created by ladin on 18.02.15.
- */
class TestDataHelper {
/**
@@ -30,7 +28,7 @@ class TestDataHelper {
return initCode()
}
- String scriptLocation = "${System.getProperty('java.io.tmpdir')}/mc/${tempSqlFileName}"
+ String scriptLocation = "${System.getProperty('java.io.tmpdir')}/${Metadata.getCurrent().getApplicationName()}/${Metadata.getCurrent().getApplicationVersion()}/${tempSqlFileName}"
if (new File(scriptLocation).exists()) {
long start = System.currentTimeMillis()
@@ -43,7 +41,7 @@ class TestDataHelper {
long start = System.currentTimeMillis()
if (drop) {
- String clearScriptLocation = "${System.getProperty('java.io.tmpdir')}/mc/dropfiles/$tempSqlFileName"
+ String clearScriptLocation = "${System.getProperty('java.io.tmpdir')}/${Metadata.getCurrent().getApplicationName()}/${Metadata.getCurrent().getApplicationVersion()}/dropfiles/$tempSqlFileName"
new Sql(sessionFactory.currentSession.connection()).execute("SCRIPT NODATA DROP TO ${clearScriptLocation}")
println "Clear script created in $clearScriptLocation"
new Sql(sessionFactory.currentSession.connection()).execute("RUNSCRIPT FROM ${clearScriptLocation}")
diff --git a/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/AbstractIntegrationSpec.groovy b/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/AbstractIntegrationSpec.groovy
index a3f733e925..db5b47a627 100644
--- a/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/AbstractIntegrationSpec.groovy
+++ b/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/AbstractIntegrationSpec.groovy
@@ -11,23 +11,23 @@ abstract class AbstractIntegrationSpec extends IntegrationSpec {
@Shared
def fixtureLoader, fixtures, initCatalogueService, sessionFactory
- def loadMarshallers() {
+ void loadMarshallers() {
def springContext = WebApplicationContextUtils.getWebApplicationContext( ServletContextHolder.servletContext )
springContext.getBean('modelCatalogueCorePluginCustomObjectMarshallers').register()
}
- def initRelationshipTypes(){
+ void initRelationshipTypes(){
TestDataHelper.initFreshDb(sessionFactory, 'reltypes.sql') {
initCatalogueService.initDefaultRelationshipTypes()
}
}
- def initCatalogue(){
+ void initCatalogue(){
initCatalogueService.initCatalogue(true)
}
- def loadFixtures(){
+ void loadFixtures(){
TestDataHelper.initFreshDb(sessionFactory, 'testdata.sql') {
initCatalogueService.initDefaultRelationshipTypes()
fixtures = fixtureLoader.load("assets/*", "batches/*", "dataTypes/*", "enumeratedTypes/*", "measurementUnits/*", "models/*", "relationshipTypes/*", "classifications/*").load("actions/*", "valueDomains/*", "users/*").load("dataElements/*").load("extensions/*", "mappings/*").load("csvTransformations/*")
diff --git a/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/export/inventory/ModelToDocxExporterSpec.groovy b/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/export/inventory/ModelToDocxExporterSpec.groovy
new file mode 100644
index 0000000000..5e53a80c10
--- /dev/null
+++ b/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/export/inventory/ModelToDocxExporterSpec.groovy
@@ -0,0 +1,101 @@
+package org.modelcatalogue.core.export.inventory
+
+import static org.modelcatalogue.core.util.test.FileOpener.open
+
+import grails.test.spock.IntegrationSpec
+import org.junit.Rule
+import org.junit.rules.TemporaryFolder
+import org.modelcatalogue.core.Classification
+import org.modelcatalogue.core.ClassificationService
+import org.modelcatalogue.core.ElementService
+import org.modelcatalogue.core.InitCatalogueService
+import org.modelcatalogue.core.Model
+import org.modelcatalogue.core.ModelService
+import org.modelcatalogue.core.ValueDomain
+import org.modelcatalogue.core.util.builder.DefaultCatalogueBuilder
+
+class ModelToDocxExporterSpec extends IntegrationSpec {
+
+ ElementService elementService
+ ClassificationService classificationService
+ InitCatalogueService initCatalogueService
+ ModelService modelService
+
+ def setup() {
+ initCatalogueService.initDefaultRelationshipTypes()
+ }
+
+ @Rule TemporaryFolder temporaryFolder
+
+ def "export model to docx"() {
+ when:
+ File file = temporaryFolder.newFile("${System.currentTimeMillis()}.docx")
+ Model model = buildTestModel()
+
+
+ new ModelToDocxExporter(model, modelService).export(file.newOutputStream())
+
+
+ open file
+
+ then:
+ noExceptionThrown()
+
+ }
+
+
+
+ private Model buildTestModel() {
+ DefaultCatalogueBuilder builder = new DefaultCatalogueBuilder(classificationService, elementService)
+
+ Random random = new Random()
+ List domains = ValueDomain.list()
+
+ if (!domains) {
+ for (int i in 1..10) {
+ ValueDomain domain = new ValueDomain(name: "C4CTDE Test Value Domain #${i}").save(failOnError: true)
+ Classification classification = new Classification(name: "C4CTDE Classification ${System.currentTimeMillis()}").save(failOnError: true)
+ classification.addToClassifies domain
+ }
+ domains = ValueDomain.list()
+ }
+
+ builder.build {
+ classification(name: 'C4CTDE') {
+ description "This is a classification for testing ClassificationToDocxExporter"
+
+ model (name: 'C4CTDE Root') {
+ for (int i in 1..10) {
+ model name: "C4CTDE Model $i", {
+ description "This is a description for Model $i"
+
+ for (int j in 1..10) {
+ dataElement name: "C4CTDE Model $i Data Element $j", {
+ description "This is a description for Model $i Data Element $j"
+ ValueDomain domain = domains[random.nextInt(domains.size())]
+ valueDomain name: domain.name, classification: domain.classifications ? domains.classifications.first().name : null
+ }
+ }
+ for (int j in 1..3) {
+ model name: "C4CTDE Model $i Child Model $j", {
+ description "This is a description for Model $i Child Model $j"
+
+ for (int k in 1..3) {
+ dataElement name: "C4CTDE Model $i Child Model $j Data Element $k", {
+ description "This is a description for Model $i Child Model $j Data Element $k"
+ ValueDomain domain = domains[random.nextInt(domains.size())]
+ valueDomain name: domain.name, classification: domain.classifications ? domains.classifications.first().name : null
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return Model.findByName('C4CTDE Root')
+
+ }
+}
diff --git a/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/export/inventory/ModelToXlsxExporterSpec.groovy b/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/export/inventory/ModelToXlsxExporterSpec.groovy
new file mode 100644
index 0000000000..1efeabadf0
--- /dev/null
+++ b/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/export/inventory/ModelToXlsxExporterSpec.groovy
@@ -0,0 +1,97 @@
+package org.modelcatalogue.core.export.inventory
+
+import grails.test.spock.IntegrationSpec
+
+import static org.modelcatalogue.core.util.test.FileOpener.open
+
+import org.junit.Rule
+import org.junit.rules.TemporaryFolder
+import org.modelcatalogue.core.*
+import org.modelcatalogue.core.util.builder.DefaultCatalogueBuilder
+
+class ModelToXlsxExporterSpec extends IntegrationSpec {
+
+ ElementService elementService
+ ClassificationService classificationService
+ ModelService modelService
+ InitCatalogueService initCatalogueService
+
+ @Rule TemporaryFolder temporaryFolder
+
+ def setup() {
+ initCatalogueService.initDefaultRelationshipTypes()
+ }
+
+ def "export model to excel"() {
+ when:
+ File file = temporaryFolder.newFile("${System.currentTimeMillis()}.xlsx")
+ Model model = buildTestModel()
+
+
+ new ModelToXlsxExporter(model, modelService).export(file.newOutputStream())
+
+
+ open file
+
+ then:
+ noExceptionThrown()
+
+ }
+
+
+
+ private Model buildTestModel() {
+ DefaultCatalogueBuilder builder = new DefaultCatalogueBuilder(classificationService, elementService)
+
+ Random random = new Random()
+ List domains = ValueDomain.list()
+
+ if (!domains) {
+ for (int i in 1..10) {
+ ValueDomain domain = new ValueDomain(name: "C4CTXE Test Value Domain #${i}").save(failOnError: true)
+ Classification classification = new Classification(name: "C4CTXE Classification ${System.currentTimeMillis()}").save(failOnError: true)
+ classification.addToClassifies domain
+ }
+ domains = ValueDomain.list()
+ }
+
+ builder.build {
+ classification(name: 'C4CTXE') {
+ description "This is a classification for testing ClassificationToDocxExporter"
+
+ model(name: 'C4CTXE Root') {
+ for (int i in 1..10) {
+ model name: "C4CTXE Model $i", {
+ description "This is a description for Model $i"
+
+ for (int j in 1..10) {
+ dataElement name: "C4CTXE Model $i Data Element $j", {
+ description "This is a description for Model $i Data Element $j"
+ ValueDomain domain = domains[random.nextInt(domains.size())]
+ valueDomain name: domain.name, classification: domain.classifications ? domains.classifications.first().name : null
+ }
+ }
+ for (int j in 1..3) {
+ model name: "C4CTXE Model $i Child Model $j", {
+ description "This is a description for Model $i Child Model $j"
+
+ for (int k in 1..3) {
+ dataElement name: "C4CTXE Model $i Child Model $j Data Element $k", {
+ description "This is a description for Model $i Child Model $j Data Element $k"
+ ValueDomain domain = domains[random.nextInt(domains.size())]
+ valueDomain name: domain.name, classification: domain.classifications ? domains.classifications.first().name : null
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return Model.findByName('C4CTXE Root')
+
+ }
+
+}
diff --git a/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/publishing/changelog/ChangelogGeneratorSpec.groovy b/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/publishing/changelog/ChangelogGeneratorSpec.groovy
new file mode 100644
index 0000000000..31c1613fb8
--- /dev/null
+++ b/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/publishing/changelog/ChangelogGeneratorSpec.groovy
@@ -0,0 +1,140 @@
+package org.modelcatalogue.core.publishing.changelog
+
+import static org.modelcatalogue.core.util.test.FileOpener.open
+
+import org.junit.Rule
+import org.junit.rules.TemporaryFolder
+import org.modelcatalogue.core.AbstractIntegrationSpec
+import org.modelcatalogue.core.ClassificationService
+import org.modelcatalogue.core.ElementService
+import org.modelcatalogue.core.Model
+import org.modelcatalogue.core.ModelService
+import org.modelcatalogue.core.ValueDomain
+import org.modelcatalogue.core.audit.AuditService
+import org.modelcatalogue.core.ddl.DataDefinitionLanguage
+import org.modelcatalogue.core.publishing.DraftContext
+import org.modelcatalogue.core.util.builder.DefaultCatalogueBuilder
+
+class ChangelogGeneratorSpec extends AbstractIntegrationSpec {
+
+ AuditService auditService
+ ModelService modelService
+ ClassificationService classificationService
+ ElementService elementService
+
+ @Rule TemporaryFolder tmp
+
+ def setup() {
+ initCatalogueService.initDefaultRelationshipTypes()
+ }
+
+ def "test changelog export"() {
+ Model draft = buildTestModel()
+
+ when:
+ File file = tmp.newFile('changelog.docx')
+
+ ChangelogGenerator generator = new ChangelogGenerator(auditService, modelService)
+
+ generator.generateChangelog(draft, file.newOutputStream())
+
+ open(file)
+
+ then:
+ noExceptionThrown()
+ }
+
+ private Model buildTestModel() {
+ DefaultCatalogueBuilder builder = new DefaultCatalogueBuilder(classificationService, elementService)
+
+ Random random = new Random()
+
+
+ builder.build {
+ classification(name: 'C4C') {
+ description "This is a classification for testing ClassificationToDocxExporter"
+
+ ext 'foo', 'bar'
+ ext 'one', '1'
+
+ model name: 'Root Model', {
+ for (int i in 1..3) {
+ model name: "Model $i", {
+ description "This is a description for Model $i"
+ ext 'foo', 'bar'
+ ext 'boo', 'cow'
+
+ for (int j in 1..3) {
+ dataElement name: "Model $i Data Element $j", {
+
+ valueDomain name: "Test Value Domain ${j}", {
+ dataType name: "Test Value Domain ${i} Data Type", enumerations: (1..(i * j)).collectEntries { ["$it", "value of $it"] }
+ }
+ relationship {
+ ext 'Min Occurs': '0', 'Max Occurs': "$j"
+ }
+ }
+ }
+ for (int j in 1..3) {
+ model name: "Model $i Child Model $j", {
+ description "This is a description for Model $i Child Model $j"
+
+ for (int k in 1..3) {
+ dataElement name: "Model $i Child Model $j Data Element $k", {
+ description "This is a description for Model $i Child Model $j Data Element $k"
+ valueDomain name: "Test Value Domain ${k}"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return makeChanges(elementService.finalizeElement(Model.findByName('Root Model')))
+
+ }
+
+ private Model makeChanges(Model finalized) {
+ Model model = elementService.createDraftVersion(finalized, DraftContext.userFriendly())
+
+ // update description of C4C to
+ DataDefinitionLanguage.with('C4C') {
+ update 'description' of 'C4C' to 'This is a classification for testing ClassificationToDocxExporter. And now it has been changed.'
+ update 'foo' of 'C4C' to 'baz'
+ update 'boo' of 'C4C' to 'dar'
+ update 'one' of 'C4C' to null
+
+ create Model called 'Model XYZ', description: 'This is Model XYZ'
+ update 'containment' of 'Model XYZ' add 'Model 1 Data Element 2', 'Min Occurs': 0, 'Max Occurs': 2
+ update 'containment' of 'Model XYZ' add 'Model 1 Data Element 3', 'Min Occurs': 0, 'Max Occurs': 3
+ update 'containment' of 'Model XYZ' add 'Model 2 Child Model 3 Data Element 1', Name: 'M2CH3DE1'
+
+ create draft of 'Model 1 Child Model 2'
+ update 'hierarchy' of 'Model 1 Child Model 2' add 'Model XYZ'
+ update 'base' of 'Model 1 Child Model 1' add 'Model 1 Child Model 2' // 'Model 1 Child Model 1' is base for 'Model 1 Child Model 2'
+
+ create draft of 'Model 1'
+ update 'hierarchy' of 'Model 1' remove 'Model 1 Child Model 1'
+ update 'containment' of 'Model 1' remove 'Model 1 Data Element 2'
+
+ update 'description' of 'Model 1 Child Model 2 Data Element 1' to 'This is a description for Model 1 Child Model 2 Data Element 1 And now it has been changed.'
+
+ create ValueDomain called 'New Value Domain'
+
+ create draft of 'Model 1 Child Model 2 Data Element 1'
+ update 'valueDomain' of 'Model 1 Child Model 2 Data Element 1' to 'New Value Domain'
+ update 'dataType' of 'Test Value Domain 1' to 'Test Value Domain 2 Data Type'
+ update 'enumerations' of 'Test Value Domain 3 Data Type' to one: 'jedna', eight: 'osm'
+
+ finalize 'C4C'
+
+ }
+
+ return model
+
+ }
+
+}
diff --git a/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/reports/ReportsRegistryIntegrationSpec.groovy b/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/reports/ReportsRegistryIntegrationSpec.groovy
index 4c06e42b68..f6c480d79c 100644
--- a/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/reports/ReportsRegistryIntegrationSpec.groovy
+++ b/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/reports/ReportsRegistryIntegrationSpec.groovy
@@ -49,8 +49,8 @@ class ReportsRegistryIntegrationSpec extends IntegrationSpec {
expect:
modelReports.size() >= 1
- modelReports[0].getTitle(model) == 'Export All Elements of Test to Excel XSLX'
- modelReports[0].getLink(model) == "/api/modelCatalogue/core/dataArchitect/getSubModelElements?format=xlsx&report=NHIC&asset=true&name=Export+All+Elements+of+Test+to+Excel+XSLX&id=${model.id}"
+ modelReports[0].getTitle(model) == 'Inventory Report Document'
+ modelReports[0].getLink(model) == "/api/modelCatalogue/core/gel/reports/inventoryDoc?id=1"
when:
def models = new Elements(itemType: Model)
diff --git a/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/specs/InheritanceSpec.groovy b/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/specs/InheritanceSpec.groovy
new file mode 100644
index 0000000000..d392281cf6
--- /dev/null
+++ b/ModelCatalogueCorePlugin/test/integration/org/modelcatalogue/core/specs/InheritanceSpec.groovy
@@ -0,0 +1,399 @@
+package org.modelcatalogue.core.specs
+
+import grails.test.spock.IntegrationSpec
+import org.modelcatalogue.builder.api.CatalogueBuilder
+import org.modelcatalogue.core.CatalogueElement
+import org.modelcatalogue.core.Model
+import org.modelcatalogue.core.DataElement
+import org.modelcatalogue.core.Classification
+import org.modelcatalogue.core.ValueDomain
+import org.modelcatalogue.core.ElementService
+import org.modelcatalogue.core.InitCatalogueService
+import org.modelcatalogue.core.Relationship
+import org.modelcatalogue.core.util.Inheritance
+import spock.lang.Ignore
+
+class InheritanceSpec extends IntegrationSpec {
+
+ public static final String DUMMY_DATA_CLASS_NAME = 'Dummy'
+ public static final String TEST_PARENT_DATA_CLASS_NAME = 'Test Parent Class'
+ public static final String TEST_DATA_ELEMENT_1_NAME = 'Test Data Element 1'
+ public static final String TEST_DATA_ELEMENT_2_NAME = 'Test Data Element 2'
+ public static final String TEST_DATA_ELEMENT_3_NAME = 'Test Data Element 3'
+ public static final String TEST_DATA_ELEMENT_4_NAME = 'Test Data Element 4'
+ public static final String METADATA_KEY_1 = 'one'
+ public static final String METADATA_KEY_2 = 'two'
+ public static final String METADATA_KEY_3 = 'three'
+ public static final String METADATA_KEY_4 = 'four'
+ public static final String METADATA_KEY_5 = 'five'
+ public static final String METADATA_VALUE_1 = '1'
+ public static final String METADATA_VALUE_2 = '2'
+ public static final String METADATA_VALUE_3 = '3'
+ public static final String METADATA_VALUE_4 = '4'
+ public static final String METADATA_VALUE_5 = '5'
+ public static final String METADATA_VALUE_5_ALT = 'V'
+ public static final String TEST_CHILD_DATA_CLASS_NAME = 'Test Child Class'
+ public static final String TEST_PARENT_VALUE_DOMAIN_NAME = 'Test Parent Value Domain'
+ public static final String TEST_CHILD_VALUE_DOMAIN_NAME = 'Test Child Value Domain'
+ public static final String TEST_DATA_TYPE_1_NAME = 'Test Data Type 1'
+ public static final String TEST_DATA_TYPE_2_NAME = 'Test Data Type 2'
+ public static final String TEST_DATA_MODEL_1_NAME = 'Test Data Model 1'
+ public static final String TEST_DATA_MODEL_2_NAME = 'Test Data Model 2'
+
+ InitCatalogueService initCatalogueService
+ ElementService elementService
+ CatalogueBuilder catalogueBuilder
+
+ Model parentClass
+ Model childClass
+ Model dummyClass
+ DataElement dataElement1
+ DataElement dataElement2
+ DataElement dataElement3
+ DataElement dataElement4
+ DataElement parentDataElement
+ DataElement childDataElement
+ ValueDomain valueDomain1
+ ValueDomain valueDomain2
+ Classification dataModel1
+ Classification dataModel2
+
+ def setup() {
+ initCatalogueService.initDefaultRelationshipTypes()
+ catalogueBuilder.build {
+ classification name: TEST_DATA_MODEL_1_NAME, {
+ model name: DUMMY_DATA_CLASS_NAME
+ model name: TEST_PARENT_DATA_CLASS_NAME, {
+ dataElement name: TEST_DATA_ELEMENT_1_NAME
+ dataElement name: TEST_DATA_ELEMENT_2_NAME
+ dataElement name: TEST_DATA_ELEMENT_3_NAME
+ ext METADATA_KEY_1, METADATA_VALUE_1
+ ext METADATA_KEY_2, METADATA_VALUE_2
+ ext METADATA_KEY_3, METADATA_VALUE_3
+ rel 'synonym' to model called DUMMY_DATA_CLASS_NAME
+ }
+ dataElement name: TEST_PARENT_VALUE_DOMAIN_NAME, {
+ valueDomain name: TEST_DATA_TYPE_1_NAME
+ }
+ dataElement name: TEST_CHILD_VALUE_DOMAIN_NAME
+ valueDomain name: TEST_DATA_TYPE_2_NAME
+ }
+
+ classification name: TEST_DATA_MODEL_2_NAME, {
+ model name: TEST_CHILD_DATA_CLASS_NAME, {
+ dataElement name: TEST_DATA_ELEMENT_4_NAME
+ ext METADATA_KEY_4, METADATA_VALUE_4
+ }
+ }
+ }
+
+ parentClass = Model.findByName(TEST_PARENT_DATA_CLASS_NAME)
+ childClass = Model.findByName(TEST_CHILD_DATA_CLASS_NAME)
+ dummyClass = Model.findByName(DUMMY_DATA_CLASS_NAME)
+ dataElement1 = DataElement.findByName(TEST_DATA_ELEMENT_1_NAME)
+ dataElement2 = DataElement.findByName(TEST_DATA_ELEMENT_2_NAME)
+ dataElement3 = DataElement.findByName(TEST_DATA_ELEMENT_3_NAME)
+ dataElement4 = DataElement.findByName(TEST_DATA_ELEMENT_4_NAME)
+ parentDataElement = DataElement.findByName(TEST_PARENT_VALUE_DOMAIN_NAME)
+ childDataElement = DataElement.findByName(TEST_CHILD_VALUE_DOMAIN_NAME)
+ valueDomain1 = ValueDomain.findByName(TEST_DATA_TYPE_1_NAME)
+ valueDomain2 = ValueDomain.findByName(TEST_DATA_TYPE_2_NAME)
+ dataModel1 = Classification.findByName(TEST_DATA_MODEL_1_NAME)
+ dataModel2 = Classification.findByName(TEST_DATA_MODEL_2_NAME)
+
+ assertNothingInherited()
+ }
+
+ def "with children works"(){
+ addBasedOn()
+
+ List children = []
+ Inheritance.withChildren(parentClass) {
+ children << it
+ }
+
+ expect:
+ children == [childClass]
+ }
+
+ def "with all children works"(){
+ addBasedOn()
+
+ List children = []
+ Inheritance.withAllChildren(parentClass) {
+ children << it
+ }
+
+ expect:
+ children == [childClass]
+ }
+
+ def "with parents works"(){
+ addBasedOn()
+
+ List parents = []
+ Inheritance.withParents(childClass) {
+ parents << it
+ }
+
+ expect:
+ parents == [parentClass]
+ }
+
+ def "with all parents works"(){
+ addBasedOn()
+
+ List parents = []
+ Inheritance.withAllParents(childClass) {
+ parents << it
+ }
+
+ expect:
+ parents == [parentClass]
+ }
+
+ def "Inherit relationships"() {
+ addBasedOn()
+ expect: "version specific relationships are inherited"
+ parentClass.countContains() == 3
+ childClass.countContains() == 4
+
+ and: "semantic links aren't"
+ parentClass.countIsSynonymFor() == 1
+ childClass.countIsSynonymFor() == 0
+
+ when: "we remove relationships from parent"
+ parentClass.removeFromContains dataElement1
+ parentClass.removeFromContains dataElement2
+
+ then: "they are removed from the parent"
+ !(dataElement1 in parentClass.contains)
+ !(dataElement2 in parentClass.contains)
+
+ and: "they are removed from child as well"
+ !(dataElement1 in childClass.contains)
+ !(dataElement2 in childClass.contains)
+
+ when: "we add relationships to the parent"
+ Relationship rp1 = parentClass.addToContains dataElement1, metadata: [(METADATA_KEY_5): METADATA_VALUE_5]
+ Relationship rp2 = parentClass.addToContains dataElement2
+ Relationship rc1 = childClass.containsRelationships.find { it.destination == dataElement1 } as Relationship
+ Relationship rc2 = childClass.containsRelationships.find { it.destination == dataElement2 } as Relationship
+
+ then: "they are added to the parent"
+ rp1
+ rp2
+ dataElement1 in parentClass.contains
+ dataElement2 in parentClass.contains
+ rp1.ext[METADATA_KEY_5] == METADATA_VALUE_5
+
+ and: "they are added to child as well"
+ rc1
+ rc1.inherited
+ rc2
+ rc2.inherited
+ dataElement1 in childClass.contains
+ dataElement2 in childClass.contains
+
+ when: "metadata in the child relationship are overridden"
+ rc1.ext[METADATA_KEY_5] = METADATA_VALUE_5_ALT
+
+ and: "the relation is removed from the parent"
+ parentClass.removeFromContains dataElement1
+
+ then: "the relation is persisted in the child as it was already customized"
+ dataElement1 in childClass.contains
+
+ when: "metadata are added to the parent relationship"
+ rp2.ext[METADATA_KEY_5] = METADATA_VALUE_5
+
+ then: "the metadata are added to child relationships as well"
+ rc2.ext[METADATA_KEY_5] == METADATA_VALUE_5
+
+ when: "we try to remove relationship from child"
+ Relationship rc3 = childClass.removeFromContains dataElement3
+
+ then: "error is returned"
+ rc3.errors.errorCount > 0
+
+ and: "the children still contain the data element"
+ dataElement3 in childClass.contains
+
+ when: "we try to add relationship which is already inherited"
+ childClass.addToContains dataElement3
+
+ then: "error is returned"
+ thrown(IllegalArgumentException)
+
+
+
+
+ when:
+ removeBasedOn()
+
+ then:
+ !(dataElement2 in childClass.contains)
+ !(dataElement3 in childClass.contains)
+ }
+
+ def "Inheriting relationships does not steal the relationships from finalized item"() {
+ elementService.finalizeElement(parentClass)
+ addBasedOn()
+ expect: "version specific relationships are inherited"
+ parentClass.countContains() == 3
+ childClass.countContains() == 4
+ }
+
+ def "inherit metadata"() {
+ addBasedOn()
+ expect: "metadata are inherited"
+ parentClass.ext.size() == 3
+ childClass.ext.size() == 4
+
+ when: "we remove extension from parent"
+ parentClass.ext.remove(METADATA_KEY_1)
+
+ then: "it is removed from the child as well"
+ !childClass.ext.get(METADATA_KEY_1)
+
+ when: "we add extension to the parent"
+ parentClass.ext[METADATA_KEY_5] = METADATA_VALUE_5
+
+ then: "it is added to child as well"
+ childClass.ext[METADATA_KEY_5] == METADATA_VALUE_5
+
+ when: "extension in the child is overridden"
+ childClass.ext[METADATA_KEY_5] = METADATA_VALUE_5_ALT
+
+ and: "the extension is removed from the parent"
+ parentClass.ext.remove(METADATA_KEY_5)
+
+ then: "the extension is persisted in the child as it was already customized"
+ childClass.ext[METADATA_KEY_5] == METADATA_VALUE_5_ALT
+
+ when:
+ removeBasedOn()
+
+ then:
+ !childClass.ext[METADATA_KEY_1]
+ !childClass.ext[METADATA_KEY_2]
+ !childClass.ext[METADATA_KEY_3]
+ }
+
+ def "inherit associations"() {
+ addBasedOn()
+ expect: "associations are inherited"
+ parentDataElement.valueDomain == valueDomain1
+ parentDataElement.save(failOnError: true, flush: true)
+ childDataElement.valueDomain == valueDomain1
+ childDataElement.save(failOnError: true, flush: true)
+
+ when: "we remove associations from parent"
+ parentDataElement.valueDomain = null
+ parentDataElement.save(failOnError: true, flush: true)
+
+ then: "it is removed from the child as well"
+ childDataElement.valueDomain == null
+
+ when: "we add association to the parent"
+ parentDataElement.valueDomain = valueDomain2
+ parentDataElement.save(failOnError: true, flush: true)
+
+ then: "it is added to child as well"
+ childDataElement.valueDomain == valueDomain2
+
+ when: "association in the child is overridden"
+ childDataElement.valueDomain = valueDomain1
+ childDataElement.save(failOnError: true, flush: true)
+
+ then: "it doesn't affect the parent"
+ parentDataElement.valueDomain == valueDomain2
+
+ when: "the association is removed from the parent"
+ parentDataElement.valueDomain = null
+ parentDataElement.save(failOnError: true, flush: true)
+
+ then: "the association is persisted in the child as it was already customized"
+ childDataElement.valueDomain == valueDomain1
+
+ when: "the association is assigned in the parent but also exist in child"
+ parentDataElement.valueDomain = valueDomain2
+ parentDataElement.save(failOnError: true, flush: true)
+
+ then: "only parent is assigned"
+ parentDataElement.valueDomain == valueDomain2
+ childDataElement.valueDomain == valueDomain1
+
+ when: "the association is removed from the child"
+ childDataElement.valueDomain = null
+ childDataElement.save(failOnError: true, flush: true)
+
+ then: "the association is reset to the one from parent"
+ childDataElement.valueDomain == valueDomain2
+
+ when: "the element no longer inherits form the parent"
+ removeBasedOn()
+
+ then: "the association is set to null"
+ childDataElement.valueDomain == null
+
+ }
+
+ def "handle data models"() {
+ expect: "both data classes belongs to right models"
+ dataModel1 in parentClass.classifications
+ dataModel2 in childClass.classifications
+
+ when:
+ addBasedOn()
+
+ then:
+ !(dataModel1 in childClass.classifications)
+ }
+
+
+ @Ignore
+ def "handle multiple inheritance"() {
+ expect: "to be implemented"
+ false
+ }
+
+ private void addBasedOn() {
+ childClass.addToIsBasedOn parentClass
+ childDataElement.addToIsBasedOn parentDataElement
+ }
+
+ private void removeBasedOn() {
+ childClass.removeFromIsBasedOn parentClass
+ childDataElement.removeFromIsBasedOn parentDataElement
+ }
+
+ private void assertNothingInherited() {
+ assert parentClass
+ assert parentClass.countIsSynonymFor() == 1
+ assert parentClass.countContains() == 3
+ assert parentClass.ext.size() == 3
+
+ assert childClass
+ assert childClass.countContains() == 1
+ assert childClass.extensions.size() == 1
+
+ assert dummyClass
+
+ assert dataElement1
+ assert dataElement2
+ assert dataElement3
+ assert dataElement4
+
+ assert parentDataElement
+ assert childDataElement
+ assert valueDomain1
+ assert valueDomain2
+
+ assert dataModel1
+ assert dataModel2
+
+ assert parentDataElement.valueDomain == valueDomain1
+ assert childDataElement.valueDomain == null
+ }
+}
diff --git a/ModelCatalogueCorePlugin/test/js/modelcatalogue/core/ui/bs/catalogueElementTreeviewItem.spec.coffee b/ModelCatalogueCorePlugin/test/js/modelcatalogue/core/ui/bs/catalogueElementTreeviewItem.spec.coffee
index 612f1a266a..2ee7116da7 100644
--- a/ModelCatalogueCorePlugin/test/js/modelcatalogue/core/ui/bs/catalogueElementTreeviewItem.spec.coffee
+++ b/ModelCatalogueCorePlugin/test/js/modelcatalogue/core/ui/bs/catalogueElementTreeviewItem.spec.coffee
@@ -13,9 +13,11 @@ describe "mc.core.ui.catalogueElementTreeviewItem", ->
$rootScope.element = catEl
$rootScope.descend = ['valueDomains']
+ $rootScope.treeview =
+ select: (element) -> console.log element
element = $compile('''
-
+
''')($rootScope)
$rootScope.$digest()
diff --git a/ModelCatalogueCorePlugin/test/unit/org/modelcatalogue/core/util/delayable/DelayableSpec.groovy b/ModelCatalogueCorePlugin/test/unit/org/modelcatalogue/core/util/delayable/DelayableSpec.groovy
new file mode 100644
index 0000000000..b1a3be92d3
--- /dev/null
+++ b/ModelCatalogueCorePlugin/test/unit/org/modelcatalogue/core/util/delayable/DelayableSpec.groovy
@@ -0,0 +1,102 @@
+package org.modelcatalogue.core.util.delayable
+
+import spock.lang.Specification
+
+
+class DelayableSpec extends Specification {
+
+ def "Can delay execution"() {
+ List collector = []
+ Delayable> delayable = new Delayable>(collector)
+
+ when:
+ collector.add 'a'
+
+ then:
+ collector.join('') == 'a'
+
+ when:
+ delayable.add 'b'
+
+ then:
+ collector.join('') == 'ab'
+
+ when:
+ delayable.pauseAndRecord()
+ delayable.add 'c'
+ delayable.add 'd'
+
+ then:
+ collector.join('') == 'ab'
+
+ when:
+ delayable.run()
+
+ then:
+ collector.join('') == 'abcd'
+
+ when:
+ delayable.pauseAndRecord()
+ delayable.add 'e'
+ delayable.resetAndUnpause()
+ delayable.add 'f'
+
+ then:
+ collector.join('') == 'abcdf'
+
+ }
+
+ def "complex nesting"() {
+ List collector = []
+ Delayable> delayable = new Delayable>(collector)
+
+ delayable.whilePaused {
+ delayable.add 'l0a'
+ delayable.whilePaused {
+ for (int i in 1..3) {
+ delayable.add(' l1b' + i)
+ delayable.whilePaused {
+ delayable.add(' l2b' + i)
+ if (i == 2) {
+ delayable.requestRun()
+ }
+ delayable.add(' l2c' + i)
+ delayable.whilePaused {
+ for (int j in 1..3) {
+ delayable.add(' l3c' + i + 'x' + j)
+ delayable.whilePaused {
+ delayable.add(' l4d' + i + 'x' + j)
+ if (i == 3) {
+ delayable.requestRun()
+ }
+ delayable.add(' l4e' + i + 'x' + j)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ expect:
+ collector.join('\n') == '''
+ l0a
+ l1b1
+ l1b2
+ l2b2
+ l2c2
+ l1b3
+ l2b3
+ l2c3
+ l3c3x1
+ l4d3x1
+ l4e3x1
+ l3c3x2
+ l4d3x2
+ l4e3x2
+ l3c3x3
+ l4d3x3
+ l4e3x3
+ '''.stripIndent().trim()
+ }
+
+}
diff --git a/ModelCatalogueCorePluginTestApp/grails-app/conf/Config.groovy b/ModelCatalogueCorePluginTestApp/grails-app/conf/Config.groovy
index 62c2d341d7..2fa9ac2a6e 100644
--- a/ModelCatalogueCorePluginTestApp/grails-app/conf/Config.groovy
+++ b/ModelCatalogueCorePluginTestApp/grails-app/conf/Config.groovy
@@ -203,6 +203,7 @@ log4j = {
debug 'org.modelcatalogue.core.publishing'
debug 'org.modelcatalogue.core.util.test'
debug 'org.modelcatalogue.core.gel'
+ debug 'org.modelcatalogue.core.export'
debug 'org.modelcatalogue.discourse'
// debug 'org.codehaus.groovy.grails.web.mapping'
diff --git a/ModelCatalogueCorePluginTestApp/grails-app/conf/GrailsMelodyConfig.groovy b/ModelCatalogueCorePluginTestApp/grails-app/conf/GrailsMelodyConfig.groovy
index ef8be4d799..49dd9f8825 100644
--- a/ModelCatalogueCorePluginTestApp/grails-app/conf/GrailsMelodyConfig.groovy
+++ b/ModelCatalogueCorePluginTestApp/grails-app/conf/GrailsMelodyConfig.groovy
@@ -24,7 +24,7 @@ javamelody.'http-transform-pattern' = '\\d+' //filter out numbers from URI
/*
The parameter url-exclude-pattern is a regular expression to exclude some urls from monitoring as written above.
*/
-javamelody.'url-exclude-pattern' = '/assets/.*|.*/download.*|.*/gereportDoc.*'
+javamelody.'url-exclude-pattern' = '/assets/.*|.*/download.*|.*/gereportDoc.*|.*/changelogDoc.*'
/*
Specify jndi name of datasource to monitor in production environment
diff --git a/ModelCatalogueCorePluginTestApp/test/functional/org/modelcatalogue/core/ModelWizardSpec.groovy b/ModelCatalogueCorePluginTestApp/test/functional/org/modelcatalogue/core/ModelWizardSpec.groovy
index c36fe96214..188c4eebc5 100644
--- a/ModelCatalogueCorePluginTestApp/test/functional/org/modelcatalogue/core/ModelWizardSpec.groovy
+++ b/ModelCatalogueCorePluginTestApp/test/functional/org/modelcatalogue/core/ModelWizardSpec.groovy
@@ -148,7 +148,11 @@ class ModelWizardSpec extends AbstractModelCatalogueGebSpec {
then:
waitFor {
- !$('span.catalogue-element-treeview-name', text: startsWith("New")).displayed && menuItem('classifications', 'navigation-bottom-left').text().contains('XMLSchema')
+ !$('span.catalogue-element-treeview-name', text: startsWith("New")).displayed
+ }
+
+ waitFor {
+ menuItem('classifications', 'navigation-bottom-left').text().contains('XMLSchema')
}
when:
diff --git a/ModelCatalogueDiscoursePlugin/grails-app/services/org/modelcatalogue/discourse/DiscourseService.groovy b/ModelCatalogueDiscoursePlugin/grails-app/services/org/modelcatalogue/discourse/DiscourseService.groovy
index 350b8327d5..9b890a25c4 100644
--- a/ModelCatalogueDiscoursePlugin/grails-app/services/org/modelcatalogue/discourse/DiscourseService.groovy
+++ b/ModelCatalogueDiscoursePlugin/grails-app/services/org/modelcatalogue/discourse/DiscourseService.groovy
@@ -1,16 +1,22 @@
package org.modelcatalogue.discourse
import grails.util.GrailsNameUtils
+import org.jsoup.Jsoup
import org.modelcatalogue.core.CatalogueElement
import org.modelcatalogue.core.Classification
import org.modelcatalogue.core.LogoutListener
+import org.modelcatalogue.core.comments.Comment
+import org.modelcatalogue.core.comments.CommentsService
import org.modelcatalogue.core.security.User
+import java.text.SimpleDateFormat
-class DiscourseService implements LogoutListener {
+class DiscourseService implements LogoutListener, CommentsService {
static transactional = false
+ private static final SimpleDateFormat JSON_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX")
+
def grailsApplication
def modelCatalogueSecurityService
@@ -68,6 +74,23 @@ class DiscourseService implements LogoutListener {
new CategoriesForClassifications(discourseCategoryName: name, classificationId: classificationId).save(flush: true).discourseCategoryName
}
+ @Override
+ List getComments(CatalogueElement element) {
+ return discourse.topics.getTopic(findOrCreateDiscourseTopic(element.getId()))?.data?.post_stream?.posts?.collect {
+ new Comment(
+ username: it.display_username,
+ text: Jsoup.parse(it.cooked).text(),
+ created: JSON_FORMAT.parse(it.created_at),
+ original: it
+ )
+ } ?: []
+ }
+
+ @Override
+ boolean isForumEnabled() {
+ return discourseEnabled
+ }
+
Long findOrCreateDiscourseTopic(Long catalogueElementId) {
TopicsForElements topic = TopicsForElements.findByCatalogueElementId(catalogueElementId)
diff --git a/ModelCatalogueGenomicsPlugin/grails-app/conf/BuildConfig.groovy b/ModelCatalogueGenomicsPlugin/grails-app/conf/BuildConfig.groovy
index fb9cf94008..0f097c61c1 100644
--- a/ModelCatalogueGenomicsPlugin/grails-app/conf/BuildConfig.groovy
+++ b/ModelCatalogueGenomicsPlugin/grails-app/conf/BuildConfig.groovy
@@ -27,7 +27,6 @@ grails.project.dependency.resolution = {
grailsCentral()
mavenCentral()
// mavenLocal()
- mavenRepo "http://dl.bintray.com/musketyr/document-builder"
// uncomment the below to enable remote dependency resolution
// from public Maven repositories
//mavenRepo "http://repository.codehaus.org"
@@ -37,9 +36,6 @@ grails.project.dependency.resolution = {
dependencies {
// specify dependencies here under either 'build', 'compile', 'runtime', 'test' or 'provided' scopes eg.
// runtime 'mysql:mysql-connector-java:5.1.27'
- //----- Jasper Reports specific dependencies
- //from the bintray
- compile 'com.craigburke.document:word:0.4.10-fix31'
}
plugins {
diff --git a/ModelCatalogueGenomicsPlugin/grails-app/conf/ModelCatalogueGenomicsUrlMappings.groovy b/ModelCatalogueGenomicsPlugin/grails-app/conf/ModelCatalogueGenomicsUrlMappings.groovy
index abec8fc625..212fe1c491 100644
--- a/ModelCatalogueGenomicsPlugin/grails-app/conf/ModelCatalogueGenomicsUrlMappings.groovy
+++ b/ModelCatalogueGenomicsPlugin/grails-app/conf/ModelCatalogueGenomicsUrlMappings.groovy
@@ -6,7 +6,6 @@ class ModelCatalogueGenomicsUrlMappings {
static mappings = {
"/api/modelCatalogue/core/gel/generateXmlShredderModel/$id"(controller: "gelXml", action: "generateXmlShredderModel", method: HttpMethod.GET)
"/api/modelCatalogue/core/gel/generateXSD/$id"(controller: "gelXml", action: "generateXSD", method: HttpMethod.GET)
- "/api/modelCatalogue/core/gel/reports/classificationJasper"(controller: 'classificationReports', action: 'gereportDoc', method: HttpMethod.GET)
}
}
\ No newline at end of file
diff --git a/ModelCatalogueGenomicsPlugin/grails-app/controllers/org/modelcatalogue/core/gel/ClassificationReportsController.groovy b/ModelCatalogueGenomicsPlugin/grails-app/controllers/org/modelcatalogue/core/gel/ClassificationReportsController.groovy
index 435b81877b..a51a9494c4 100644
--- a/ModelCatalogueGenomicsPlugin/grails-app/controllers/org/modelcatalogue/core/gel/ClassificationReportsController.groovy
+++ b/ModelCatalogueGenomicsPlugin/grails-app/controllers/org/modelcatalogue/core/gel/ClassificationReportsController.groovy
@@ -1,80 +1,26 @@
package org.modelcatalogue.core.gel
-import java.util.concurrent.ExecutorService
-
-
-import org.modelcatalogue.core.Asset
+import org.modelcatalogue.core.Model
+import org.modelcatalogue.core.ModelService
+import org.modelcatalogue.core.export.inventory.ModelToDocxExporter
+import org.modelcatalogue.core.publishing.changelog.ChangelogGenerator
import org.modelcatalogue.core.AssetService
-import org.modelcatalogue.core.Classification
-import org.modelcatalogue.core.SecurityService;
-import org.modelcatalogue.core.api.ElementStatus;
import org.modelcatalogue.core.audit.AuditService
/**
* Various reports generations as an asset.
- *
+ *
*
*/
class ClassificationReportsController {
- ExecutorService executorService
AuditService auditService
AssetService assetService
- SecurityService modelCatalogueSecurityService
-
- def index() { }
-
- def gereportDoc() {
- Classification classification = Classification.get(params.id)
-
- def assetId=storeAssetAsDocx(classification)
+ ModelService modelService
- response.setHeader("X-Asset-ID",assetId.toString())
- redirect controller: 'asset', id: assetId, action: 'show'
- }
-
-
- private def storeAssetAsDocx(Classification classification){
- Asset asset = new Asset(
- name: "$classification.name report as MS Word Document",
- originalFileName: "${classification.name}-${classification.status}-${classification.version}.docx",
- description: "Your classification report will be available in this asset soon. Use Refresh action to reload",
- status: ElementStatus.PENDING,
- contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
- size: 0
- )
-
- asset.save(flush: true, failOnError: true)
-
- Long id = asset.id
- Long authorId = modelCatalogueSecurityService.currentUser?.id
- Long classificationId = classification.id
+ def index() { }
- executorService.submit {
- auditService.withDefaultAuthorId(authorId) {
- Asset updated = Asset.get(id)
- try {
- //do the hard work
- assetService.storeAssetWithSteam(updated, "application/vnd.openxmlformats-officedocument.wordprocessingml.document",) { OutputStream out ->
- new ClassificationToDocxExporter(Classification.get(classificationId)).export(out)
- }
-
- updated.status = ElementStatus.FINALIZED
- updated.description = "Your classification is ready. Use Download button to download it."
- updated.save(flush: true, failOnError: true)
- } catch (e) {
- log.error "Exception of type ${e.class} with id=${id}", e
- updated.refresh()
- updated.status = ElementStatus.FINALIZED
- updated.name = updated.name + " - Error during generation"
- updated.description = "Error generating classification report" +":$e"
- updated.save(flush: true, failOnError: true)
- }
- }
- }
- return asset.id;
- }
}
diff --git a/ModelCatalogueGenomicsPlugin/test/integration/org/modelcatalogue/core/gel/ClassificationToDocxExporterSpec.groovy b/ModelCatalogueGenomicsPlugin/test/integration/org/modelcatalogue/core/gel/ClassificationToDocxExporterSpec.groovy
deleted file mode 100644
index 67e7b56046..0000000000
--- a/ModelCatalogueGenomicsPlugin/test/integration/org/modelcatalogue/core/gel/ClassificationToDocxExporterSpec.groovy
+++ /dev/null
@@ -1,103 +0,0 @@
-package org.modelcatalogue.core.gel
-
-import grails.test.spock.IntegrationSpec
-import org.junit.Rule
-import org.junit.rules.TemporaryFolder
-import org.modelcatalogue.core.Classification
-import org.modelcatalogue.core.ClassificationService
-import org.modelcatalogue.core.ElementService
-import org.modelcatalogue.core.InitCatalogueService
-import org.modelcatalogue.core.ValueDomain
-import org.modelcatalogue.core.util.builder.DefaultCatalogueBuilder
-
-class ClassificationToDocxExporterSpec extends IntegrationSpec {
-
- ElementService elementService
- ClassificationService classificationService
- InitCatalogueService initCatalogueService
-
- def setup() {
- initCatalogueService.initDefaultRelationshipTypes()
- }
-
- @Rule TemporaryFolder temporaryFolder
-
- def "export model to docx"() {
- when:
- File file = temporaryFolder.newFile("${System.currentTimeMillis()}.docx")
- Classification classification = buildTestClassification()
-
-
- new ClassificationToDocxExporter(classification).export(file.newOutputStream())
-
-
- println file.absolutePath
-
- then:
- noExceptionThrown()
-
- }
-
-
-
- private Classification buildTestClassification() {
- DefaultCatalogueBuilder builder = new DefaultCatalogueBuilder(classificationService, elementService)
-
- Random random = new Random()
- List domains = ValueDomain.list()
-
- if (!domains) {
- for (int i in 1..10) {
- ValueDomain domain = new ValueDomain(name: "Test Value Domain #${i}").save(failOnError: true)
- Classification classification = new Classification(name: "Classification ${System.currentTimeMillis()}").save(failOnError: true)
- classification.addToClassifies domain
- }
- domains = ValueDomain.list()
- }
-
- builder.build {
- classification(name: 'C4CTDE') {
- description "This is a classification for testing ClassificationToDocxExporter"
-
- for (int i in 1..10) {
- model name: "Model $i", {
- description "This is a description for Model $i"
-
- for (int j in 1..10) {
- dataElement name: "Model $i Data Element $j", {
- description "This is a description for Model $i Data Element $j"
- ValueDomain domain = domains[random.nextInt(domains.size())]
- while (!domain.classifications) {
- domain = domains[random.nextInt(domains.size())]
- }
- valueDomain name: domain.name, classification: domain.classifications.first().name
- }
- }
- for (int j in 1..3) {
- model name: "Model $i Child Model $j", {
- description "This is a description for Model $i Child Model $j"
-
- for (int k in 1..3) {
- dataElement name: "Model $i Child Model $j Data Element $k", {
- description "This is a description for Model $i Child Model $j Data Element $k"
- ValueDomain domain = domains[random.nextInt(domains.size())]
- while (!domain.classifications) {
- domain = domains[random.nextInt(domains.size())]
- }
- valueDomain name: domain.name, classification: domain.classifications.first().name
- }
- }
- }
- }
- }
- }
-
-
- }
- }
-
- return Classification.findByName('C4CTDE')
-
- }
-
-}