Our first endpoint

September 28th, 2020


This is the 3rd article of a Build Java Module for Mango series. You can check all the articles by clicking here

To create our first endpoint, we need to create the next files first:

  • DeviceAuditEvent
  • DeviceVO
  • DeviceDao
  • DeviceService
  • DeviceModel
  • DeviceModelMapping
  • DeviceRestController

DeviceAuditEvent

This is used for creating custom audit event types. In our case, we will place this file inside /src/com/infiniteautomation/energyMetering, and this is the content of the file:

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

This class transform our values into a Java object, we will place this file inside /src/com/infiniteautomation/energyMetering/vo, and its content:

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();

    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 is a structural pattern that allows us to isolate the application/business layer from the persistence layer (usually a relational database, but it could be any other persistence mechanism) using an abstract API. In our case, this class will have all the business logic to make CRUD operations to the Device. We will place this file inside com/infiniteautomation/mango/spring/dao and this is its content:

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.Condition;
import org.jooq.Record;
import org.jooq.SelectJoinStep;
import org.jooq.Table;
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> {
    private static final LazyInitSupplier<DeviceDao> springInstance = new LazyInitSupplier<>(() -> {
        Object o = Common.getRuntimeContext().getBean(DeviceDao.class);
        if (o == null) {
            throw new ShouldNeverHappenException("DAO not initialized in Spring Runtime Context");
        }
        return (DeviceDao)o;
    });

    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;
        }
    }
}
  • savePreRelationalData is executed before we make an INSERT or UPDATE into the database. In this method we add the permissions.
  • saveRelationalData is executed after we make an INSERT or UPDATE into the database. In this method we check if there is an existing VO (it just happens when we make an update) and remove the old permissions if they have changed.
  • loadRelationalData is executed after we make a query to the database. In our case, we load the permissions into the VO.
  • deletePostRelationalData is executed after we delete a device from the database. In our case, we remove the related permissions.
  • voToObjectArray is used to convert from VO to an ObjectArray, so this array is passed into the insert and update methods.
  • getRowMapper is used to convert the data from the database to the VO.

DeviceService

It basically adds a validation layer of the incoming data. We will place this file inside com/infiniteautomation/mango/spring/service and this is its content:

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;
    }
}
  • validate methods are used to be sure that the permissions are correct before inserting / updating the data into the database.

DeviceModel

We will place this file inside com/infiniteautomation/mango/rest/latest/model and this is its content:

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();
    }

    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 from DeviceVO to DeviceModel and viceversa. We will place this file inside com/infiniteautomation/mango/rest/latest/mapping and this is its content:

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

Finally, the DeviceRestController class defines the endpoints that we are going to use. In this case, we are going to create one for insert a new devices, and another to get a device by XID. This is the code:

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);
    }
}

Now, after you build the module, you can test the endpoints from Swagger UI (you need to enable it on the env.porperties file). You will see something like this:

Endpoints in Swagger

Copyright © 2020 Radix IoT, LLC.