Skip to content

Commit

Permalink
Enable specifying target type when created using DMF API
Browse files Browse the repository at this point in the history
Extension of DMF API with possibility of setting target
type name when creating target. If a target type with the
provided name is found (was created beforehand) then it
is associated with the new target.

Signed-off-by: Ondrej Charvat <[email protected]>
  • Loading branch information
charvadzo committed Nov 8, 2023
1 parent ac946e7 commit ae2c208
Show file tree
Hide file tree
Showing 11 changed files with 233 additions and 27 deletions.
13 changes: 13 additions & 0 deletions docs/content/apis/dmf_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Payload Template (optional):
```json
{
"name": "String",
"type": "String",
"attributeUpdate": {
"attributes": {
"exampleKey1" : "exampleValue1",
Expand All @@ -74,6 +75,18 @@ Payload Template (optional):
```

The "name" property specifies the name of the thing, which by default is the thing ID. This property is optional.<br />
<br />
The "type" property specifies name of a target type which should be assigned to the created/updated target. The
target type with the specified name should be created in advance, otherwise it can't be assigned to the target,
resulting in:
* error is logged
* if the target does not exist then it is created without any target type assigned
* if it exists already then no changes to its target type assignment are made.

If the "type" property is set to a blank string while updating an existing target then any eventual target type
assignment is removed from the target. This property is optional and if omitted then no changes to the target type
assignment are made.<br />
<br />
The "attributeUpdate" property provides the attributes of the thing, for details see UPDATE_ATTRIBUTES message. This property is optional.


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,14 +231,18 @@ private void registerTarget(final Message message, final String virtualHost) {
final URI amqpUri = IpUtil.createAmqpUri(virtualHost, replyTo);
final Target target;
if (isOptionalMessageBodyEmpty(message)) {
LOG.debug("Received \"THING_CREATED\" AMQP message for thing \"{}\" without body.", thingId);
target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(thingId, amqpUri);
} else {
checkContentTypeJson(message);
final DmfCreateThing thingCreateBody = convertMessage(message, DmfCreateThing.class);
final DmfAttributeUpdate thingAttributeUpdateBody = thingCreateBody.getAttributeUpdate();

LOG.debug("Received \"THING_CREATED\" AMQP message for thing \"{}\" with target name \"{}\" and type " +
"\"{}\".", thingId, thingCreateBody.getName(), thingCreateBody.getType());

target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(thingId, amqpUri,
thingCreateBody.getName());
thingCreateBody.getName(), thingCreateBody.getType());

if (thingAttributeUpdateBody != null) {
controllerManagement.updateControllerAttributes(thingId, thingAttributeUpdateBody.getAttributes(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
import org.eclipse.hawkbit.repository.builder.ActionStatusBuilder;
import org.eclipse.hawkbit.repository.builder.ActionStatusCreate;
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
import org.eclipse.hawkbit.repository.jpa.builder.JpaActionStatusBuilder;
import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus;
import org.eclipse.hawkbit.repository.jpa.model.helper.SecurityTokenGeneratorHolder;
Expand Down Expand Up @@ -163,6 +162,9 @@ public class AmqpMessageHandlerServiceTest {
@Captor
private ArgumentCaptor<String> targetNameCaptor;

@Captor
private ArgumentCaptor<String> targetTypeNameCaptor;

@Captor
private ArgumentCaptor<URI> uriCaptor;

Expand Down Expand Up @@ -224,7 +226,8 @@ private void processThingCreatedMessage(final String thingId, final DmfCreateThi
uriCaptor.capture())).thenReturn(targetMock);
} else {
when(controllerManagementMock.findOrRegisterTargetIfItDoesNotExist(targetIdCaptor.capture(),
uriCaptor.capture(), targetNameCaptor.capture())).thenReturn(targetMock);
uriCaptor.capture(), targetNameCaptor.capture(), targetTypeNameCaptor.capture()))
.thenReturn(targetMock);
if (payload.getAttributeUpdate() != null) {
when(controllerManagementMock.updateControllerAttributes(targetIdCaptor.capture(),
attributesCaptor.capture(), modeCaptor.capture())).thenReturn(null);
Expand Down Expand Up @@ -276,6 +279,27 @@ private void assertThingNameCapturedField(final String thingName) {
assertThat(targetNameCaptor.getValue()).as("Thing name is wrong").isEqualTo(thingName);
}

@Test
@Description("Tests the creation of a target/thing with specified type name by calling the same method that incoming RabbitMQ messages would access.")
public void createThingWithType() {
final String knownThingId = "2";
final String knownThingTypeName = "TargetTypeName";

final DmfCreateThing payload = new DmfCreateThing();
payload.setType(knownThingTypeName);

processThingCreatedMessage(knownThingId, payload);

assertThingIdCapturedField(knownThingId);
assertReplyToCapturedField("MyTest");
assertThingTypeCapturedField(knownThingTypeName);
}

@Step
private void assertThingTypeCapturedField(final String thingType) {
assertThat(targetTypeNameCaptor.getValue()).as("Thing type is wrong").isEqualTo(thingType);
}

@Test
@Description("Tests not allowed body in message")
public void createThingWithWrongBody() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public final class SoftwareModuleJsonMatcher {

/**
* Creates a matcher that matches when the list of repository software
* modules arelogically equal to the specified JSON software modules.
* modules are logically equal to the specified JSON software modules.
* <p>
* If the specified repository software modules are <code>null</code> then
* the created matcher will only match if the JSON software modules are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public class DmfCreateThing {
@JsonProperty
private String name;

@JsonProperty
private String type;

@JsonProperty
private DmfAttributeUpdate attributeUpdate;

Expand All @@ -35,6 +38,14 @@ public void setName(final String name) {
this.name = name;
}

public String getType() {
return type;
}

public void setType(final String type) {
this.type = type;
}

public DmfAttributeUpdate getAttributeUpdate() {
return attributeUpdate;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,8 @@ Map<Long, List<SoftwareModuleMetadata>> findTargetVisibleMetaDataBySoftwareModul
/**
* Register new target in the repository (plug-and-play) and in case it
* already exists updates {@link Target#getAddress()} and
* {@link Target#getLastTargetQuery()} and {@link Target#getName()} and
* {@link Target#getLastTargetQuery()} and {@link Target#getName()}
* and {@link Target#getTargetType()} and
* switches if {@link TargetUpdateStatus#UNKNOWN} to
* {@link TargetUpdateStatus#REGISTERED}.
*
Expand All @@ -233,10 +234,13 @@ Map<Long, List<SoftwareModuleMetadata>> findTargetVisibleMetaDataBySoftwareModul
* the client IP address of the target, might be {@code null}
* @param name
* the name of the target
* @param type
* the target type name of the target
* @return target reference
*/
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
Target findOrRegisterTargetIfItDoesNotExist(@NotEmpty String controllerId, @NotNull URI address, String name);
Target findOrRegisterTargetIfItDoesNotExist(@NotEmpty String controllerId, @NotNull URI address, String name,
String type);

/**
* Retrieves last {@link Action} for a download of an artifact of given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public interface TargetTypeManagement {
* as {@link TargetType#getName()}
* @return {@link TargetType}
*/
@PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_READ_TARGET)
@PreAuthorize(SpPermission.SpringEvalExpressions.IS_CONTROLLER_OR_HAS_AUTH_READ_TARGET)
Optional<TargetType> getByName(@NotEmpty String name);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import org.eclipse.hawkbit.repository.RepositoryConstants;
import org.eclipse.hawkbit.repository.RepositoryProperties;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.TargetTypeManagement;
import org.eclipse.hawkbit.repository.UpdateMode;
import org.eclipse.hawkbit.repository.builder.ActionStatusCreate;
import org.eclipse.hawkbit.repository.event.remote.CancelTargetAssignmentEvent;
Expand Down Expand Up @@ -78,6 +79,7 @@
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetType;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder;
import org.eclipse.hawkbit.security.SystemSecurityContext;
Expand Down Expand Up @@ -154,6 +156,9 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
@Autowired
private ConfirmationManagement confirmationManagement;

@Autowired
private TargetTypeManagement targetTypeManagement;

public JpaControllerManagement(final ScheduledExecutorService executorService,
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
final QuotaManagement quotaManagement, final RepositoryProperties repositoryProperties) {
Expand Down Expand Up @@ -377,28 +382,43 @@ public void deleteExistingTarget(@NotEmpty final String controllerId) {
@Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(include = ConcurrencyFailureException.class, exclude = EntityAlreadyExistsException.class, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public Target findOrRegisterTargetIfItDoesNotExist(final String controllerId, final URI address) {
return findOrRegisterTargetIfItDoesNotExist(controllerId, address, null);
return findOrRegisterTargetIfItDoesNotExist(controllerId, address, null, null);
}

@Override
@Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(include = ConcurrencyFailureException.class, exclude = EntityAlreadyExistsException.class, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public Target findOrRegisterTargetIfItDoesNotExist(final String controllerId, final URI address,
final String name) {
final Specification<JpaTarget> spec = (targetRoot, query, cb) -> cb
.equal(targetRoot.get(JpaTarget_.controllerId), controllerId);
final String name, final String type) {
final Specification<JpaTarget> spec =
(targetRoot, query, cb) -> cb.equal(targetRoot.get(JpaTarget_.controllerId), controllerId);

return targetRepository.findOne(spec).map(target -> updateTarget(target, address, name))
.orElseGet(() -> createTarget(controllerId, address, name));
return targetRepository.findOne(spec).map(target -> updateTarget(target, address, name, type))
.orElseGet(() -> createTarget(controllerId, address, name, type));
}

private Target createTarget(final String controllerId, final URI address, final String name) {
private Target createTarget(final String controllerId, final URI address, final String name, final String type) {

final Target result = targetRepository.save((JpaTarget) entityFactory.target().create()
LOG.debug("Creating target for thing ID \"{}\".", controllerId);
JpaTarget jpaTarget = (JpaTarget) entityFactory.target().create()
.controllerId(controllerId).description("Plug and Play target: " + controllerId)
.name((StringUtils.hasText(name) ? name : controllerId)).status(TargetUpdateStatus.REGISTERED)
.lastTargetQuery(System.currentTimeMillis())
.address(Optional.ofNullable(address).map(URI::toString).orElse(null)).build());
.address(Optional.ofNullable(address).map(URI::toString).orElse(null)).build();


if (StringUtils.hasText(type)) {
var targetTypeOptional = targetTypeManagement.getByName(type);
if (targetTypeOptional.isPresent()) {
LOG.debug("Setting target type for thing ID \"{}\" to \"{}\".", controllerId, type);
jpaTarget.setTargetType(targetTypeOptional.get());
} else {
LOG.error("Target type with the provided name \"{}\" was not found. Creating target for thing ID" +
" \"{}\" without target type assignment", type, controllerId);
}
}

final Target result = targetRepository.save(jpaTarget);

afterCommit.afterCommit(() -> eventPublisherHolder.getEventPublisher()
.publishEvent(new TargetPollEvent(result, eventPublisherHolder.getApplicationId())));
Expand Down Expand Up @@ -494,14 +514,30 @@ private static String formatQueryInStatementParams(final Collection<String> para
* or the buffer queue is full.
*
*/
private Target updateTarget(final JpaTarget toUpdate, final URI address, final String name) {
if (isStoreEager(toUpdate, address, name) || !queue.offer(new TargetPoll(toUpdate))) {
private Target updateTarget(final JpaTarget toUpdate, final URI address, final String name, final String type) {
if (isStoreEager(toUpdate, address, name, type) || !queue.offer(new TargetPoll(toUpdate))) {
if (isAddressChanged(toUpdate.getAddress(), address)) {
toUpdate.setAddress(address.toString());
}
if (isNameChanged(toUpdate.getName(), name)) {
toUpdate.setName(name);
}

if (isTypeChanged(toUpdate.getTargetType(), type)) {
if (StringUtils.hasText(type)) {
var targetTypeOptional = targetTypeManagement.getByName(type);
if (targetTypeOptional.isPresent()) {
LOG.debug("Updating target type for thing ID \"{}\" to \"{}\".", toUpdate.getControllerId(), type);
toUpdate.setTargetType(targetTypeOptional.get());
} else {
LOG.error("Target type with the provided name \"{}\" was not found. Target type for thing ID" +
" \"{}\" will not be updated", type, toUpdate.getControllerId());
}
} else {
LOG.debug("Removing target type assignment for thing ID \"{}\".", toUpdate.getControllerId());
toUpdate.setTargetType(null); //unassign target type if "" target type name was provided
}
}
if (isStatusUnknown(toUpdate.getUpdateStatus())) {
toUpdate.setUpdateStatus(TargetUpdateStatus.REGISTERED);
}
Expand All @@ -513,9 +549,10 @@ private Target updateTarget(final JpaTarget toUpdate, final URI address, final S
return toUpdate;
}

private boolean isStoreEager(final JpaTarget toUpdate, final URI address, final String name) {
private boolean isStoreEager(final JpaTarget toUpdate, final URI address, final String name, final String type) {
return repositoryProperties.isEagerPollPersistence() || isAddressChanged(toUpdate.getAddress(), address)
|| isNameChanged(toUpdate.getName(), name) || isStatusUnknown(toUpdate.getUpdateStatus());
|| isNameChanged(toUpdate.getName(), name) || isTypeChanged(toUpdate.getTargetType(), type)
|| isStatusUnknown(toUpdate.getUpdateStatus());
}

private static boolean isAddressChanged(final URI addressToUpdate, final URI address) {
Expand All @@ -526,6 +563,10 @@ private static boolean isNameChanged(final String nameToUpdate, final String nam
return StringUtils.hasText(name) && !nameToUpdate.equals(name);
}

private static boolean isTypeChanged(final TargetType targetTypeToUpdate, final String type) {
return (type != null) && (targetTypeToUpdate == null || !targetTypeToUpdate.getName().equals(type));
}

private static boolean isStatusUnknown(final TargetUpdateStatus statusToUpdate) {
return TargetUpdateStatus.UNKNOWN == statusToUpdate;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType;
import org.eclipse.hawkbit.repository.jpa.specifications.TargetTypeSpecification;
import org.eclipse.hawkbit.repository.model.TargetType;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
Expand Down Expand Up @@ -186,8 +187,11 @@ default List<JpaTargetType> findByDsType(@Param("id") final Long dsTypeId) {
*
* @param name
* to search for
* @return all {@link TargetType}s in the repository with given
* {@link TargetType#getName()}
* @return a single {@link TargetType} from the repository with given
* {@link TargetType#getName()} or empty Optional if none is found
*
* @throws IncorrectResultSizeDataAccessException – if more than one target
* type with the given name is found.
*/
default Optional<JpaTargetType> findByName(final String name) {
return this.findOne(Specification.where(TargetTypeSpecification.hasName(name)));
Expand Down
Loading

0 comments on commit ae2c208

Please sign in to comment.