diff --git a/Mango API/RELEASE-NOTES b/Mango API/RELEASE-NOTES index aa230c5ad..dbb601242 100644 --- a/Mango API/RELEASE-NOTES +++ b/Mango API/RELEASE-NOTES @@ -1,5 +1,6 @@ *Version 3.6.2* * Fix bug where bookends were not being applied to Simplified data ranges with no values in them +* Add copy endpoint for data sources PUT /rest/v2/data-sources/copy/ *Version 3.6.1* * Improve performance by reducing garbage generation for point value rollup requests on the v2 endpoints diff --git a/Mango API/api-test/dataSource.spec.js b/Mango API/api-test/dataSource.spec.js index 183735538..280450fc2 100644 --- a/Mango API/api-test/dataSource.spec.js +++ b/Mango API/api-test/dataSource.spec.js @@ -16,10 +16,39 @@ */ const config = require('@infinite-automation/mango-client/test/setup'); +const uuidV4 = require('uuid/v4'); describe('Data source service', () => { before('Login', config.login); + const newDataPoint = (xid, dsXid, rollupType, simplifyType, simplifyValue) => { + return new DataPoint({ + xid: xid, + enabled: true, + name: 'Point values test', + deviceName: 'Point values test', + dataSourceXid : dsXid, + pointLocator : { + startValue : '0', + modelType : 'PL.VIRTUAL', + dataType : 'NUMERIC', + changeType : 'NO_CHANGE', + settable: true + }, + textRenderer: { + type: 'textRendererAnalog', + format: '0.00', + suffix: '', + useUnitAsSuffix: false, + unit: '', + renderedUnit: '' + }, + rollup: rollupType, + simplifyType: simplifyType, + simplifyTolerance: simplifyValue + }); + }; + it('Gets the internal data source', () => { return DataSource.get('internal_mango_monitoring_ds').then(ds => { assert.equal(ds.xid, 'internal_mango_monitoring_ds'); @@ -80,5 +109,57 @@ describe('Data source service', () => { assert.equal(dsList[0].name, 'Mango Internal'); }); }); + + it('Copies a data source and points', function() { + this.ds = new DataSource({ + xid: uuidV4(), + name: 'Mango client test', + enabled: true, + modelType: 'VIRTUAL', + pollPeriod: { periods: 5, type: 'HOURS' }, + purgeSettings: { override: false, frequency: { periods: 1, type: 'YEARS' } }, + alarmLevels: { POLL_ABORTED: 'URGENT' }, + editPermission: null + }); + return this.ds.save().then((savedDs) => { + assert.strictEqual(savedDs.name, 'Mango client test'); + assert.isNumber(savedDs.id); + }).then(() => { + this.testPoint1 = newDataPoint(uuidV4(), this.ds.xid, 'FIRST', 'NONE', 0); + this.testPoint2 = newDataPoint(uuidV4(), this.ds.xid, 'FIRST', 'NONE', 0); + this.testPoint3 = newDataPoint(uuidV4(), this.ds.xid, 'COUNT', 'TOLERANCE', 10.0); + this.testPoint4 = newDataPoint(uuidV4(), this.ds.xid, 'COUNT', 'NONE', 0); + return Promise.all([this.testPoint1.save(), this.testPoint2.save(), this.testPoint3.save(), this.testPoint4.save()]); + }).then(() => { + return client.restRequest({ + path: `/rest/v2/data-sources/copy/${this.ds.xid}`, + params: { + copyName: this.ds.name + '-copy', + copyXid: this.ds.xid + '-copy', + copyDeviceName: 'Mango client copy device name', + copyPoints: true, + enabled: false + }, + method: 'PUT' + }).then(response => { + assert.strictEqual(response.data.xid, this.ds.xid + '-copy'); + assert.strictEqual(response.data.name, this.ds.name + '-copy'); + assert.strictEqual(response.data.enabled, false); + return client.restRequest({ + path: `/rest/v2/data-points?dataSourceXid=${this.ds.xid}-copy`, + method: 'GET' + }).then(response => { + assert.strictEqual(response.data.total, 4); + response.data.items.forEach((dp, i) => { + assert.strictEqual(dp.deviceName, 'Mango client copy device name'); + }); + }); + }); + }); + }); + + after('Deletes the copied virtual data source and its points', function() { + return this.ds.delete(); + }); }); diff --git a/Mango API/src/com/infiniteautomation/mango/rest/v2/DataSourcesRestController.java b/Mango API/src/com/infiniteautomation/mango/rest/v2/DataSourcesRestController.java index 06be29d0a..72b82c6e4 100644 --- a/Mango API/src/com/infiniteautomation/mango/rest/v2/DataSourcesRestController.java +++ b/Mango API/src/com/infiniteautomation/mango/rest/v2/DataSourcesRestController.java @@ -14,6 +14,7 @@ import javax.servlet.http.HttpServletRequest; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -36,14 +37,19 @@ import com.infiniteautomation.mango.rest.v2.patch.PatchVORequestBody; import com.infiniteautomation.mango.spring.service.DataSourceService; import com.infiniteautomation.mango.util.RQLUtils; +import com.infiniteautomation.mango.util.exception.ValidationException; import com.serotonin.db.pair.LongLongPair; import com.serotonin.m2m2.Common; import com.serotonin.m2m2.db.dao.DataPointDao; +import com.serotonin.m2m2.db.dao.DataSourceDao; +import com.serotonin.m2m2.i18n.ProcessResult; +import com.serotonin.m2m2.i18n.TranslatableMessage; import com.serotonin.m2m2.rt.dataSource.DataSourceRT; import com.serotonin.m2m2.rt.dataSource.PollingDataSource; import com.serotonin.m2m2.vo.User; import com.serotonin.m2m2.vo.dataSource.DataSourceVO; import com.serotonin.m2m2.web.MediaTypes; +import com.serotonin.validation.StringValidation; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -251,6 +257,76 @@ public Map exportDataSource( return export; } + @ApiOperation(value = "Copy data source", notes="Copy the data source and its points with optional new XID and Name.") + @RequestMapping(method = RequestMethod.PUT, value = "/copy/{xid}") + public ResponseEntity> copy( + @PathVariable String xid, + @ApiParam(value = "Copy's new XID", required = false, defaultValue="null", allowMultiple = false) + @RequestParam(required=false, defaultValue="null") String copyXid, + @ApiParam(value = "Copy's name", required = false, defaultValue="null", allowMultiple = false) + @RequestParam(required=false, defaultValue="null") String copyName, + @ApiParam(value = "Device name for copied points", required = false, defaultValue="null", allowMultiple = false) + @RequestParam(required=false, defaultValue="null") String copyDeviceName, + @ApiParam(value = "Enable/disabled state of data source", required = false, defaultValue="false", allowMultiple = false) + @RequestParam(required=false, defaultValue="false") boolean enabled, + @ApiParam(value = "Copy data points", required = false, defaultValue="false", allowMultiple = false) + @RequestParam(required=false, defaultValue="false") boolean copyPoints, + + @AuthenticationPrincipal User user, + UriComponentsBuilder builder) { + //TODO Mango 3.7 move this logic to service + T existing = service.get(xid, user); + + //Determine the new name + String newName; + if(StringUtils.isEmpty(copyName)) { + newName = StringUtils.abbreviate( + TranslatableMessage.translate(Common.getTranslations(), "common.copyPrefix", existing.getName()), 40); + }else { + newName = copyName; + } + //Determine the new xid + String newXid; + if(StringUtils.isEmpty(copyXid)) { + newXid = DataSourceDao.getInstance().generateUniqueXid(); + }else { + newXid = copyXid; + } + + String newDeviceName; + if(StringUtils.isEmpty(copyDeviceName)) { + newDeviceName = existing.getName(); + }else { + newDeviceName = copyDeviceName; + } + //Ensure device name is valid + if (StringValidation.isLengthGreaterThan(newDeviceName, 255)) { + ProcessResult result = new ProcessResult(); + result.addMessage("deviceName", new TranslatableMessage("validate.notLongerThan", 255)); + throw new ValidationException(result); + } + + T copy = existing.copy(); + copy.setId(Common.NEW_ID); + copy.setName(newName); + copy.setXid(newXid); + copy.setEnabled(enabled); + copy.ensureValid(); + + //Save it + Common.runtimeManager.saveDataSource(copy); + + if(copyPoints) { + DataSourceDao.getInstance().copyDataSourcePoints(existing.getId(), copy.getId(), newDeviceName); + } + + URI location = builder.path("/data-sources/{xid}").buildAndExpand(newXid).toUri(); + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(location); + + return new ResponseEntity<>(map.apply(service.get(newXid, user), user), headers, HttpStatus.OK); + } + /** * Perform a query * @param rql