Creating Your First Endpoint
This is the 3rd article of the Build Java Module for Mango series. See the full series in the Module Development Overview.
To create your first REST endpoint, you need to create the following classes:
- DeviceAuditEvent -- Custom audit event type
- DeviceVO -- Value Object (Java representation of a device)
- DeviceDao -- Data Access Object (database operations)
- DeviceService -- Business logic and validation
- DeviceModel -- REST API model (JSON serialization)
- DeviceModelMapping -- Converts between VO and Model
- DeviceRestController -- HTTP endpoint definitions
DeviceAuditEvent
This class creates custom audit event types for tracking changes to devices. Place this file inside /src/com/infiniteautomation/energyMetering:
package com.infiniteautomation.energyMetering;
import com.serotonin.m2m2.module.AuditEventTypeDefinition;
public class DeviceAuditEvent extends AuditEventTypeDefinition {
public static final String TYPE_NAME = "ENMET_DEVICE";
public static final String TYPE_KEY = "energyMetering.header.device";
@Override
public String getTypeName() {
return TYPE_NAME;
}
@Override
public String getDescriptionKey() {
return TYPE_KEY;
}
}
DeviceVO
The Value Object transforms database values into a Java object. Place this file inside /src/com/infiniteautomation/energyMetering/vo:
package com.infiniteautomation.energyMetering.vo;
import com.fasterxml.jackson.databind.JsonNode;
import com.infiniteautomation.energyMetering.DeviceAuditEvent;
import com.infiniteautomation.mango.permission.MangoPermission;
import com.serotonin.json.spi.JsonProperty;
import com.serotonin.m2m2.vo.AbstractVO;
public class DeviceVO extends AbstractVO {
public static final String XID_PREFIX = "ENMET_DEVICE_";
public static final long serialVersionUID = 1L;
@JsonProperty
private String protocol;
@JsonProperty
private String make;
@JsonProperty
private String model;
@JsonProperty
private JsonNode data;
@JsonProperty()
private MangoPermission readPermission = new MangoPermission();
@JsonProperty()
private MangoPermission editPermission = new MangoPermission();
// Getters and setters
public String getProtocol() { return protocol; }
public void setProtocol(String protocol) { this.protocol = protocol; }
public String getMake() { return make; }
public void setMake(String make) { this.make = make; }
public String getModel() { return model; }
public void setModel(String model) { this.model = model; }
public JsonNode getData() { return data; }
public void setData(JsonNode data) { this.data = data; }
public MangoPermission getReadPermission() { return readPermission; }
public void setReadPermission(MangoPermission readPermission) { this.readPermission = readPermission; }
public MangoPermission getEditPermission() { return editPermission; }
public void setEditPermission(MangoPermission editPermission) { this.editPermission = editPermission; }
@Override
public String getTypeKey() {
return DeviceAuditEvent.TYPE_KEY;
}
}
DeviceDao
The Data Access Object (DAO) pattern isolates the application/business layer from the persistence layer. This class contains all the logic for CRUD operations on devices. Place this file inside com/infiniteautomation/mango/spring/dao:
package com.infiniteautomation.mango.spring.dao;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.infiniteautomation.energyMetering.DeviceAuditEvent;
import com.infiniteautomation.energyMetering.vo.DeviceVO;
import com.infiniteautomation.mango.db.query.ConditionSortLimit;
import com.infiniteautomation.mango.permission.MangoPermission;
import com.infiniteautomation.mango.spring.MangoRuntimeContextConfiguration;
import com.infiniteautomation.mango.spring.db.RoleTableDefinition;
import com.infiniteautomation.mango.spring.service.PermissionService;
import com.infiniteautomation.mango.util.LazyInitSupplier;
import com.serotonin.ShouldNeverHappenException;
import com.serotonin.m2m2.Common;
import com.serotonin.m2m2.db.dao.AbstractVoDao;
import com.serotonin.m2m2.db.dao.PermissionDao;
import com.serotonin.m2m2.db.dao.tables.MintermMappingTable;
import com.serotonin.m2m2.db.dao.tables.PermissionMappingTable;
import com.serotonin.m2m2.vo.permission.PermissionHolder;
import org.jooq.*;
import org.jooq.impl.DSL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.stream.Collectors;
@Repository
public class DeviceDao extends AbstractVoDao<DeviceVO, DevicesTableDefinition> {
// ...spring instance supplier...
private final PermissionService permissionService;
private final PermissionDao permissionDao;
@Autowired
private DeviceDao(
DevicesTableDefinition table,
PermissionService permissionService,
PermissionDao permissionDao,
@Qualifier(MangoRuntimeContextConfiguration.DAO_OBJECT_MAPPER_NAME) ObjectMapper mapper,
ApplicationEventPublisher publisher
) {
super(DeviceAuditEvent.TYPE_NAME, table, mapper, publisher);
this.permissionService = permissionService;
this.permissionDao = permissionDao;
}
@Override
protected String getXidPrefix() {
return DeviceVO.XID_PREFIX;
}
@Override
public void savePreRelationalData(DeviceVO existing, DeviceVO vo) {
permissionDao.permissionId(vo.getReadPermission());
permissionDao.permissionId(vo.getEditPermission());
}
@Override
public void saveRelationalData(DeviceVO existing, DeviceVO vo) {
if (existing != null) {
if (!existing.getReadPermission().equals(vo.getReadPermission())) {
permissionDao.permissionDeleted(existing.getReadPermission());
}
if (!existing.getEditPermission().equals(vo.getReadPermission())) {
permissionDao.permissionDeleted(existing.getEditPermission());
}
}
}
@Override
public void loadRelationalData(DeviceVO vo) {
vo.setReadPermission(permissionDao.get(vo.getReadPermission().getId()));
vo.setEditPermission(permissionDao.get(vo.getEditPermission().getId()));
}
@Override
public void deletePostRelationalData(DeviceVO vo) {
permissionDao.permissionDeleted(vo.getReadPermission(), vo.getEditPermission());
}
@Override
public <R extends Record> SelectJoinStep<R> joinPermissions(
SelectJoinStep<R> select,
ConditionSortLimit conditions,
PermissionHolder user
) {
if (!permissionService.hasAdminRole(user)) {
List<Integer> roleIds = permissionService
.getAllInheritedRoles(user)
.stream()
.map(r -> r.getId())
.collect(Collectors.toList());
Condition roleIdsIn = RoleTableDefinition.roleIdField.in(roleIds);
Table<?> mintermGranted = this.create
.select(MintermMappingTable.MINTERMS_MAPPING.mintermId)
.from(MintermMappingTable.MINTERMS_MAPPING)
.groupBy(MintermMappingTable.MINTERMS_MAPPING.mintermId)
.having(
DSL.count().eq(
DSL.count(
DSL.case_()
.when(roleIdsIn, DSL.inline(1))
.else_(DSL.inline((Integer) null))
)
)
).asTable("mintermsGranted");
Table<?> permissionGranted = this.create
.selectDistinct(PermissionMappingTable.PERMISSIONS_MAPPING.permissionId)
.from(PermissionMappingTable.PERMISSIONS_MAPPING)
.join(mintermGranted)
.on(mintermGranted.field(MintermMappingTable.MINTERMS_MAPPING.mintermId)
.eq(PermissionMappingTable.PERMISSIONS_MAPPING.mintermId))
.asTable("permissionsGranted");
select = select.join(permissionGranted)
.on(
permissionGranted
.field(PermissionMappingTable.PERMISSIONS_MAPPING.permissionId)
.in(DevicesTableDefinition.READ_PERMISSION_ALIAS)
);
}
return select;
}
@Override
protected Object[] voToObjectArray(DeviceVO vo) {
return new Object[] {
vo.getXid(),
vo.getName(),
vo.getProtocol(),
vo.getMake(),
vo.getModel(),
convertData(vo.getData()),
vo.getReadPermission().getId(),
vo.getEditPermission().getId()
};
}
@Override
public RowMapper<DeviceVO> getRowMapper() {
return new DeviceRowMapper();
}
class DeviceRowMapper implements RowMapper<DeviceVO> {
@Override
public DeviceVO mapRow(ResultSet rs, int rowNum) throws SQLException {
DeviceVO vo = new DeviceVO();
int i = 0;
vo.setId(rs.getInt(++i));
vo.setXid(rs.getString(++i));
vo.setName(rs.getString(++i));
vo.setProtocol(rs.getString(++i));
vo.setMake(rs.getString(++i));
vo.setModel(rs.getString(++i));
vo.setData(extractData(rs.getClob(++i)));
vo.setReadPermission(new MangoPermission(rs.getInt(++i)));
vo.setEditPermission(new MangoPermission(rs.getInt(++i)));
return vo;
}
}
}
Key methods explained:
savePreRelationalData-- Executed before INSERT or UPDATE. Adds the permissions.saveRelationalData-- Executed after INSERT or UPDATE. Checks if there is an existing VO (during updates) and removes old permissions if they changed.loadRelationalData-- Executed after a query. Loads the permissions into the VO.deletePostRelationalData-- Executed after a delete. Removes the related permissions.voToObjectArray-- Converts from VO to an Object array for insert and update operations.getRowMapper-- Converts database result rows back to VOs.
DeviceService
The service layer adds validation of incoming data. Place this file inside com/infiniteautomation/mango/spring/service:
package com.infiniteautomation.mango.spring.service;
import com.infiniteautomation.energyMetering.vo.DeviceVO;
import com.infiniteautomation.mango.spring.dao.DeviceDao;
import com.infiniteautomation.mango.spring.dao.DevicesTableDefinition;
import com.serotonin.m2m2.i18n.ProcessResult;
import com.serotonin.m2m2.vo.permission.PermissionHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class DeviceService extends AbstractVOService<DeviceVO, DevicesTableDefinition, DeviceDao> {
@Autowired
public DeviceService(DeviceDao deviceDao, PermissionService permissionService) {
super(deviceDao, permissionService);
}
@Override
public boolean hasEditPermission(PermissionHolder user, DeviceVO vo) {
return permissionService.hasPermission(user, vo.getEditPermission());
}
@Override
public boolean hasReadPermission(PermissionHolder user, DeviceVO vo) {
return permissionService.hasPermission(user, vo.getReadPermission());
}
@Override
public ProcessResult validate(DeviceVO vo, PermissionHolder user) {
ProcessResult result = super.validate(vo, user);
permissionService.validateVoRoles(result, "readPermission", user, false, null, vo.getReadPermission());
permissionService.validateVoRoles(result, "editPermission", user, false, null, vo.getEditPermission());
return result;
}
@Override
public ProcessResult validate(DeviceVO existing, DeviceVO vo, PermissionHolder user) {
ProcessResult result = super.validate(existing, vo, user);
permissionService.validateVoRoles(result, "readPermission", user, false, existing.getReadPermission(), vo.getReadPermission());
permissionService.validateVoRoles(result, "editPermission", user, false, existing.getEditPermission(), vo.getEditPermission());
return result;
}
}
The validate methods ensure that the permissions are correct before inserting or updating data in the database.
DeviceModel
The REST model defines the JSON structure for API requests and responses. Place this file inside com/infiniteautomation/mango/rest/latest/model:
package com.infiniteautomation.mango.rest.latest.model;
import com.fasterxml.jackson.databind.JsonNode;
import com.infiniteautomation.energyMetering.vo.DeviceVO;
import com.infiniteautomation.mango.rest.latest.model.permissions.MangoPermissionModel;
public class DeviceModel extends AbstractVoModel<DeviceVO> {
private String protocol;
private String make;
private String model;
private JsonNode data;
private MangoPermissionModel readPermission;
private MangoPermissionModel editPermission;
public DeviceModel(DeviceVO data) { fromVO(data); }
public DeviceModel() { super(); }
@Override
protected DeviceVO newVO() { return new DeviceVO(); }
// Getters and setters
public String getProtocol() { return protocol; }
public void setProtocol(String protocol) { this.protocol = protocol; }
public String getMake() { return make; }
public void setMake(String make) { this.make = make; }
public String getModel() { return model; }
public void setModel(String model) { this.model = model; }
public JsonNode getData() { return data; }
public void setData(JsonNode data) { this.data = data; }
public MangoPermissionModel getReadPermission() { return readPermission; }
public void setReadPermissions(MangoPermissionModel readPermission) { this.readPermission = readPermission; }
public MangoPermissionModel getEditPermission() { return editPermission; }
public void setEditPermissions(MangoPermissionModel editPermission) { this.editPermission = editPermission; }
}
DeviceModelMapping
This class maps between DeviceVO and DeviceModel. Place this file inside com/infiniteautomation/mango/rest/latest/mapping:
package com.infiniteautomation.mango.rest.latest.mapping;
import com.infiniteautomation.energyMetering.vo.DeviceVO;
import com.infiniteautomation.mango.rest.latest.model.DeviceModel;
import com.infiniteautomation.mango.rest.latest.model.RestModelMapper;
import com.infiniteautomation.mango.rest.latest.model.RestModelMapping;
import com.infiniteautomation.mango.rest.latest.model.permissions.MangoPermissionModel;
import com.infiniteautomation.mango.util.exception.ValidationException;
import com.serotonin.m2m2.vo.permission.PermissionHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class DeviceModelMapping implements RestModelMapping<DeviceVO, DeviceModel> {
@Autowired
DeviceModelMapping() {}
@Override
public Class<? extends DeviceVO> fromClass() { return DeviceVO.class; }
@Override
public Class<? extends DeviceModel> toClass() { return DeviceModel.class; }
@Override
public DeviceModel map(Object from, PermissionHolder user, RestModelMapper mapper) {
DeviceVO vo = (DeviceVO) from;
DeviceModel model = new DeviceModel();
model.setXid(vo.getXid());
model.setName(vo.getName());
model.setProtocol(vo.getProtocol());
model.setMake(vo.getMake());
model.setModel(vo.getModel());
model.setData(vo.getData());
model.setReadPermission(new MangoPermissionModel(vo.getReadPermission()));
model.setEditPermission(new MangoPermissionModel(vo.getEditPermission()));
return model;
}
@Override
public DeviceVO unmap(Object from, PermissionHolder user, RestModelMapper mapper)
throws ValidationException {
DeviceModel model = (DeviceModel) from;
DeviceVO vo = model.toVO();
vo.setProtocol(model.getProtocol());
vo.setMake(model.getMake());
vo.setModel(model.getModel());
vo.setData(model.getData());
vo.setReadPermission(model.getReadPermissions() != null
? model.getReadPermission().getPermission() : null);
vo.setEditPermission(model.getEditPermissions() != null
? model.getEditPermission().getPermission() : null);
return vo;
}
}
DeviceRestController
The controller defines the HTTP endpoints. This initial version includes endpoints to create a new device and get a device by XID:
package com.infiniteautomation.mango.rest.latest;
import com.infiniteautomation.energyMetering.vo.DeviceVO;
import com.infiniteautomation.mango.rest.latest.mapping.DeviceModelMapping;
import com.infiniteautomation.mango.rest.latest.model.DeviceModel;
import com.infiniteautomation.mango.rest.latest.model.RestModelMapper;
import com.infiniteautomation.mango.spring.service.DeviceService;
import com.serotonin.json.JsonException;
import com.serotonin.m2m2.vo.User;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.UriComponentsBuilder;
import java.io.IOException;
import java.net.URI;
@Api(value = "Energy Metering Devices")
@RestController()
@RequestMapping("/enmet-devices")
public class DeviceRestController {
private final DeviceService service;
private final DeviceModelMapping mapping;
private final RestModelMapper mapper;
@Autowired
DeviceRestController(DeviceService service, DeviceModelMapping mapping, RestModelMapper mapper) {
this.service = service;
this.mapping = mapping;
this.mapper = mapper;
}
@ApiOperation(value = "Get device by XID")
@RequestMapping(method = RequestMethod.GET, value = "/{xid}")
public DeviceModel get(
@ApiParam(value = "Valid device XID", required = true, allowMultiple = false)
@PathVariable String xid,
@AuthenticationPrincipal User user
) {
return mapping.map(service.get(xid), user, mapper);
}
@ApiOperation(value = "Create a new device")
@RequestMapping(method = RequestMethod.POST)
public ResponseEntity<DeviceModel> create(
@ApiParam(value = "Device model", required = true)
@RequestBody(required = true) DeviceModel model,
@AuthenticationPrincipal User user,
UriComponentsBuilder builder
) throws JsonException, IOException {
DeviceVO vo = service.insert(mapping.unmap(model, user, mapper));
URI location = builder.path("/enmet-devices/{xid}").buildAndExpand(vo.getXid()).toUri();
HttpHeaders headers = new HttpHeaders();
headers.setLocation(location);
return new ResponseEntity<>(mapping.map(vo, user, mapper), headers, HttpStatus.CREATED);
}
}
Build and Test
After building the module, you can test the endpoints from the Swagger UI (you need to enable it in the env.properties file). You will see the Energy Metering Devices endpoints listed with the available operations.
Continue to Testing Module Endpoints to write automated tests for these endpoints.
Related Pages
- Module Configuration — Previous step: database tables and schema definitions
- Testing Module Endpoints — Next step: write automated Mocha tests for your endpoints
- REST API Overview — Swagger UI and API documentation for verifying endpoints
- Custom Module Overview — Full series roadmap