diff --git a/pom.xml b/pom.xml index 457a53d6f..1d92be6f9 100644 --- a/pom.xml +++ b/pom.xml @@ -97,6 +97,16 @@ + + + + org.projectlombok + lombok + 1.18.22 + + + + diff --git a/shongo-client-cli/src/main/perl/Shongo/ClientCli/API/AuxiliaryData.pm b/shongo-client-cli/src/main/perl/Shongo/ClientCli/API/AuxiliaryData.pm new file mode 100644 index 000000000..76a29a3b2 --- /dev/null +++ b/shongo-client-cli/src/main/perl/Shongo/ClientCli/API/AuxiliaryData.pm @@ -0,0 +1,45 @@ +# +# Auxiliary data for ReservationRequestAbstract +# +# @author Filip Karnis +# +package Shongo::ClientCli::API::AuxiliaryData; +use base qw(Shongo::ClientCli::API::Object); + +use strict; +use warnings; + +use Shongo::Common; +use Shongo::Console; + +# +# Create a new instance of auxiliary data +# +# @static +# +sub new() +{ + my $class = shift; + my (%attributes) = @_; + my $self = Shongo::ClientCli::API::Object->new(@_); + bless $self, $class; + + $self->set_object_class('AuxData'); + $self->set_object_name('Auxiliary Data'); + $self->add_attribute('tagName', { + 'required' => 1, + 'type' => 'string', + }); + $self->add_attribute('enabled', { + 'required' => 1, + 'type' => 'bool', + }); + $self->add_attribute('data', { + 'required' => 0, + 'type' => 'string', + }); + + return $self; +} + +1; diff --git a/shongo-client-cli/src/main/perl/Shongo/ClientCli/API/ReservationRequestAbstract.pm b/shongo-client-cli/src/main/perl/Shongo/ClientCli/API/ReservationRequestAbstract.pm index 427f39823..013de7872 100644 --- a/shongo-client-cli/src/main/perl/Shongo/ClientCli/API/ReservationRequestAbstract.pm +++ b/shongo-client-cli/src/main/perl/Shongo/ClientCli/API/ReservationRequestAbstract.pm @@ -13,6 +13,7 @@ use Shongo::Common; use Shongo::Console; use Shongo::ClientCli::API::ReservationRequest; use Shongo::ClientCli::API::ReservationRequestSet; +use Shongo::ClientCli::API::AuxiliaryData; # Enumeration of reservation request purpose our $Purpose = ordered_hash( @@ -89,6 +90,15 @@ sub new() 'OWNED' => 'Owned' ) }); + $self->add_attribute('auxData', { + 'type' => 'collection', + 'item' => { + 'title' => 'Auxiliary Data', + 'class' => 'Shongo::ClientCli::API::AuxiliaryData', + 'short' => 1, + }, + 'optional' => 1, + }); return $self; } diff --git a/shongo-client-cli/src/main/perl/Shongo/ClientCli/ReservationService.pm b/shongo-client-cli/src/main/perl/Shongo/ClientCli/ReservationService.pm index d3e17620b..1fa47c09d 100644 --- a/shongo-client-cli/src/main/perl/Shongo/ClientCli/ReservationService.pm +++ b/shongo-client-cli/src/main/perl/Shongo/ClientCli/ReservationService.pm @@ -274,7 +274,8 @@ sub list_reservation_requests() {'field' => 'technology', 'title' => 'Technology'}, {'field' => 'allocationState', 'title' => 'Allocation'}, {'field' => 'executableState', 'title' => 'Executable'}, - {'field' => 'description', 'title' => 'Description'} + {'field' => 'description', 'title' => 'Description'}, + {'field' => 'auxData', 'title' => 'Auxiliary Data'}, ], 'data' => [] }; @@ -321,7 +322,8 @@ sub list_reservation_requests() 'technology' => $technologies, 'allocationState' => Shongo::ClientCli::API::ReservationRequest::format_state($reservation_request->{'allocationState'}), 'executableState' => Shongo::ClientCli::API::ReservationRequest::format_state($reservation_request->{'executableState'}), - 'description' => $reservation_request->{'description'} + 'description' => $reservation_request->{'description'}, + 'auxData' => $reservation_request->{'auxData'}, }); } console_print_table($table); diff --git a/shongo-client-cli/src/main/perl/Shongo/ClientCli/ResourceService.pm b/shongo-client-cli/src/main/perl/Shongo/ClientCli/ResourceService.pm index 65f2efd25..5e8029439 100644 --- a/shongo-client-cli/src/main/perl/Shongo/ClientCli/ResourceService.pm +++ b/shongo-client-cli/src/main/perl/Shongo/ClientCli/ResourceService.pm @@ -15,6 +15,15 @@ use Shongo::ClientCli::API::Resource; use Shongo::ClientCli::API::DeviceResource; use Shongo::ClientCli::API::Alias; +# +# Tag types +# +our $TagType = ordered_hash( + 'DEFAULT' => 'Default', + 'NOTIFY_EMAIL' => 'Notify Email', + 'RESERVATION_DATA' => 'Reservation Data', +); + # # Populate shell by options for management of resources. # @@ -477,6 +486,19 @@ sub create_tag() 'title' => 'Tag name', } ); + $tag->add_attribute( + 'type', { + 'required' => 1, + 'title' => 'Tag type', + 'type' => 'enum', + 'enum' => $Shongo::ClientCli::ResourceService::TagType, + } + ); + $tag->add_attribute( + 'data', { + 'title' => 'Tag data', + } + ); my $id = $tag->create($attributes, $options); if ( defined($id) ) { @@ -514,13 +536,17 @@ sub list_tags() 'columns' => [ {'field' => 'id', 'title' => 'Identifier'}, {'field' => 'name', 'title' => 'Name'}, + {'field' => 'type', 'title' => 'Type'}, + {'field' => 'data', 'title' => 'Data'}, ], 'data' => [] }; - foreach my $resource (@{$response}) { + foreach my $tag (@{$response}) { push(@{$table->{'data'}}, { - 'id' => $resource->{'id'}, - 'name' => $resource->{'name'}, + 'id' => $tag->{'id'}, + 'name' => $tag->{'name'}, + 'type' => $tag->{'type'}, + 'data' => $tag->{'data'}, }); } console_print_table($table); diff --git a/shongo-client-web/src/main/java/cz/cesnet/shongo/client/web/models/SpecificationType.java b/shongo-client-web/src/main/java/cz/cesnet/shongo/client/web/models/SpecificationType.java index 4683796c4..f74309bf7 100644 --- a/shongo-client-web/src/main/java/cz/cesnet/shongo/client/web/models/SpecificationType.java +++ b/shongo-client-web/src/main/java/cz/cesnet/shongo/client/web/models/SpecificationType.java @@ -4,6 +4,10 @@ import cz.cesnet.shongo.client.web.ClientWebConfiguration; import cz.cesnet.shongo.client.web.support.MessageProvider; import cz.cesnet.shongo.controller.api.ReservationRequestSummary; +import cz.cesnet.shongo.controller.api.Tag; + +import java.util.List; +import java.util.stream.Collectors; /** * Type of specification for a reservation request. @@ -102,15 +106,17 @@ public static SpecificationType fromReservationRequestSummary(ReservationRequest case USED_ROOM: return PERMANENT_ROOM_CAPACITY; case RESOURCE: - String resourceTags = reservationRequestSummary.getResourceTags(); + List resourceTags = reservationRequestSummary.getResourceTags() + .stream() + .map(Tag::getName) + .collect(Collectors.toList()); String parkTagName = ClientWebConfiguration.getInstance().getParkingPlaceTagName(); String vehicleTagName = ClientWebConfiguration.getInstance().getVehicleTagName(); - if (resourceTags != null) { - if (parkTagName != null && resourceTags.contains(parkTagName)) { - return PARKING_PLACE; - } else if (vehicleTagName != null && resourceTags.contains(vehicleTagName)) { - return VEHICLE; - } + if (parkTagName != null && resourceTags.contains(parkTagName)) { + return PARKING_PLACE; + } + else if (vehicleTagName != null && resourceTags.contains(vehicleTagName)) { + return VEHICLE; } return MEETING_ROOM; default: diff --git a/shongo-common/pom.xml b/shongo-common/pom.xml index 8f5d00137..60713a072 100644 --- a/shongo-common/pom.xml +++ b/shongo-common/pom.xml @@ -53,6 +53,13 @@ + + + + io.hypersistence + hypersistence-utils-hibernate-52 + 3.2.0 + diff --git a/shongo-common/src/main/java/cz/cesnet/shongo/hibernate/package-info.java b/shongo-common/src/main/java/cz/cesnet/shongo/hibernate/package-info.java index 645097e3b..67d3ab6bd 100644 --- a/shongo-common/src/main/java/cz/cesnet/shongo/hibernate/package-info.java +++ b/shongo-common/src/main/java/cz/cesnet/shongo/hibernate/package-info.java @@ -11,9 +11,11 @@ @TypeDef(name = PersistentLocalDate.NAME, typeClass = PersistentLocalDate.class), @TypeDef(name = PersistentPeriod.NAME, typeClass = PersistentPeriod.class), @TypeDef(name = PersistentInterval.NAME, typeClass = PersistentInterval.class), - @TypeDef(name = PersistentReadablePartial.NAME, typeClass = PersistentReadablePartial.class) + @TypeDef(name = PersistentReadablePartial.NAME, typeClass = PersistentReadablePartial.class), + @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class), }) package cz.cesnet.shongo.hibernate; +import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; import org.hibernate.annotations.TypeDef; import org.hibernate.annotations.TypeDefs; diff --git a/shongo-controller-api/pom.xml b/shongo-controller-api/pom.xml index bce341e6b..0ebf28667 100644 --- a/shongo-controller-api/pom.xml +++ b/shongo-controller-api/pom.xml @@ -40,6 +40,11 @@ jackson-core 2.10.1 + + org.projectlombok + lombok + provided + diff --git a/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/AbstractReservationRequest.java b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/AbstractReservationRequest.java index 3ecbde003..48fe8d986 100644 --- a/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/AbstractReservationRequest.java +++ b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/AbstractReservationRequest.java @@ -7,6 +7,9 @@ import cz.cesnet.shongo.controller.api.rpc.ReservationService; import org.joda.time.DateTime; +import java.util.ArrayList; +import java.util.List; + /** * Request for reservation of resources. * @@ -75,6 +78,11 @@ public abstract class AbstractReservationRequest extends IdentifiedComplexType */ private boolean isSchedulerDeleted = false; + /** + * Auxiliary data. This data are specified by the {@link Tag}s of {@link Resource} which is requested for reservation. + */ + private List auxData = new ArrayList<>(); + /** * Constructor. */ @@ -291,6 +299,22 @@ public void setIsSchedulerDeleted(boolean isSchedulerDeleted) this.isSchedulerDeleted = isSchedulerDeleted; } + /** + * @return {@link #auxData} + */ + public List getAuxData() + { + return auxData; + } + + /** + * @param auxData sets the {@link #auxData} + */ + public void setAuxData(List auxData) + { + this.auxData = auxData; + } + private static final String TYPE = "type"; private static final String DATETIME = "dateTime"; private static final String USER_ID = "userId"; @@ -303,6 +327,7 @@ public void setIsSchedulerDeleted(boolean isSchedulerDeleted) private static final String REUSED_RESERVATION_REQUEST_MANDATORY = "reusedReservationRequestMandatory"; private static final String REUSEMENT = "reusement"; private static final String IS_SCHEDULER_DELETED = "isSchedulerDeleted"; + public static final String AUX_DATA = "auxData"; @Override public DataMap toData() @@ -320,6 +345,7 @@ public DataMap toData() dataMap.set(REUSED_RESERVATION_REQUEST_MANDATORY, reusedReservationRequestMandatory); dataMap.set(REUSEMENT, reusement); dataMap.set(IS_SCHEDULER_DELETED, isSchedulerDeleted); + dataMap.set(AUX_DATA, auxData); return dataMap; } @@ -339,5 +365,6 @@ public void fromData(DataMap dataMap) reusedReservationRequestMandatory = dataMap.getBool(REUSED_RESERVATION_REQUEST_MANDATORY); reusement = dataMap.getEnum(REUSEMENT, ReservationRequestReusement.class); isSchedulerDeleted = dataMap.getBool(IS_SCHEDULER_DELETED); + auxData = dataMap.getList(AUX_DATA, AuxiliaryData.class); } } diff --git a/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/AuxiliaryData.java b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/AuxiliaryData.java new file mode 100644 index 000000000..d61a59e23 --- /dev/null +++ b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/AuxiliaryData.java @@ -0,0 +1,99 @@ +package cz.cesnet.shongo.controller.api; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import cz.cesnet.shongo.api.AbstractComplexType; +import cz.cesnet.shongo.api.DataMap; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class AuxiliaryData extends AbstractComplexType +{ + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private String tagName; + private boolean enabled; + @ToString.Exclude + @EqualsAndHashCode.Exclude + private String data = objectMapper.nullNode().toString(); + + public AuxiliaryData(String tagName, boolean enabled, String data) + { + setTagName(tagName); + setEnabled(enabled); + setData(data); + } + + public String getData() + { + return data; + } + + @JsonIgnore + @ToString.Include(name = "data") + @EqualsAndHashCode.Include + public JsonNode getDataAsJsonNode() + { + if (data == null) { + return null; + } + + try { + return objectMapper.readTree(data); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public void setData(String data) + { + if (data == null) { + this.data = objectMapper.nullNode().toString(); + return; + } + + this.data = data; + } + + public void setData(JsonNode data) + { + try { + this.data = objectMapper.writeValueAsString(data); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + @Override + @JsonIgnore + public String getClassName() { + return super.getClassName(); + } + + @Override + public DataMap toData() + { + DataMap dataMap = super.toData(); + dataMap.set("tagName", tagName); + dataMap.set("enabled", enabled); + dataMap.set("data", data); + return dataMap; + } + + @Override + public void fromData(DataMap dataMap) + { + super.fromData(dataMap); + tagName = dataMap.getString("tagName"); + enabled = dataMap.getBoolean("enabled"); + data = dataMap.getString("data"); + } +} diff --git a/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/ReservationRequestSummary.java b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/ReservationRequestSummary.java index d72930c69..dc0be659a 100644 --- a/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/ReservationRequestSummary.java +++ b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/ReservationRequestSummary.java @@ -110,7 +110,7 @@ public class ReservationRequestSummary extends IdentifiedComplexType /** * Resource tags. */ - private String resourceTags; + private List resourceTags = new ArrayList<>(); /** * Specifies whether room has recording service. @@ -127,20 +127,32 @@ public class ReservationRequestSummary extends IdentifiedComplexType */ private boolean allowCache = true; + /** + * Auxiliary data. This data are specified by the {@link Tag}s of {@link Resource} which is requested for reservation. + */ + private String auxData; + /** * @return {@link #resourceTags} */ - public String getResourceTags() { + public List getResourceTags() { return resourceTags; } /** * @param resourceTags sets the {@link #resourceTags} */ - public void setResourceTags(String resourceTags) { + public void setResourceTags(List resourceTags) { this.resourceTags = resourceTags; } + /** + * @param resourceTag adds tag to {@link #resourceTags} + */ + public void addResourceTag(Tag resourceTag) { + this.resourceTags.add(resourceTag); + } + /** * @return {@link #parentReservationRequestId} */ @@ -496,6 +508,22 @@ public void setAllowCache(boolean allowCache) this.allowCache = allowCache; } + /** + * @return {@link #auxData} + */ + public String getAuxData() + { + return auxData; + } + + /** + * @param auxData sets the {@link #auxData} + */ + public void setAuxData(String auxData) + { + this.auxData = auxData; + } + private static final String PARENT_RESERVATION_REQUEST_ID = "parentReservationRequestId"; private static final String TYPE = "type"; private static final String DATETIME = "dateTime"; @@ -518,6 +546,7 @@ public void setAllowCache(boolean allowCache) private static final String ROOM_HAS_RECORDINGS = "roomHasRecordings"; private static final String ALLOW_CACHE = "allowCache"; private static final String RESOURCE_TAGS = "resourceTags"; + private static final String AUX_DATA = "auxData"; @Override public DataMap toData() @@ -545,6 +574,7 @@ public DataMap toData() dataMap.set(ROOM_HAS_RECORDINGS, roomHasRecordings); dataMap.set(ALLOW_CACHE, allowCache); dataMap.set(RESOURCE_TAGS, resourceTags); + dataMap.set(AUX_DATA, auxData); return dataMap; } @@ -573,7 +603,8 @@ public void fromData(DataMap dataMap) roomHasRecordingService = dataMap.getBool(ROOM_HAS_RECORDING_SERVICE); roomHasRecordings = dataMap.getBool(ROOM_HAS_RECORDINGS); allowCache = dataMap.getBool(ALLOW_CACHE); - resourceTags = dataMap.getString(RESOURCE_TAGS); + resourceTags = dataMap.getList(RESOURCE_TAGS, Tag.class); + auxData = dataMap.getString(AUX_DATA); } /** diff --git a/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/Tag.java b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/Tag.java index d7284b6cd..364116229 100644 --- a/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/Tag.java +++ b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/Tag.java @@ -3,6 +3,8 @@ import cz.cesnet.shongo.api.DataMap; import cz.cesnet.shongo.api.IdentifiedComplexType; +import java.util.Objects; + /** * * @author Ondřej Pavelka @@ -10,6 +12,8 @@ public class Tag extends IdentifiedComplexType { String name; + TagType type = TagType.DEFAULT; + String data; public String getName() { return name; @@ -19,13 +23,37 @@ public void setName(String name) { this.name = name; } + public TagType getType() + { + return type; + } + + public void setType(TagType type) + { + this.type = type; + } + + public String getData() + { + return data; + } + + public void setData(String data) + { + this.data = data; + } + private static final String NAME = "name"; + private static final String TYPE = "type"; + private static final String DATA = "data"; @Override public DataMap toData() { DataMap dataMap = super.toData(); dataMap.set(NAME,name); + dataMap.set(TYPE, type); + dataMap.set(DATA, data); return dataMap; } @@ -34,6 +62,8 @@ public void fromData(DataMap dataMap) { super.fromData(dataMap); name = dataMap.getString(NAME); + type = dataMap.getEnumRequired(TYPE, TagType.class); + data = dataMap.getString(DATA); } @Override @@ -43,14 +73,12 @@ public boolean equals(Object o) if (o == null || getClass() != o.getClass()) return false; Tag tag = (Tag) o; - - return name.equals(tag.name); - + return Objects.equals(name, tag.name) && type == tag.type && Objects.equals(data, tag.data); } @Override public int hashCode() { - return name.hashCode(); + return Objects.hash(name, type, data); } } diff --git a/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/TagType.java b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/TagType.java new file mode 100644 index 000000000..03664d500 --- /dev/null +++ b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/TagType.java @@ -0,0 +1,23 @@ +package cz.cesnet.shongo.controller.api; + +/** + * Type of {@link cz.cesnet.shongo.controller.booking.resource.Tag}. + */ +public enum TagType +{ + /** + * Simple tag. Does not do anything special. + */ + DEFAULT, + + /** + * Sends notifications to the email addresses specified in this {@link cz.cesnet.shongo.controller.booking.resource.Tag}. + */ + NOTIFY_EMAIL, + + /** + * Adds additional information specified in {@link cz.cesnet.shongo.controller.booking.resource.Tag} + * to {@link cz.cesnet.shongo.controller.booking.reservation.Reservation}. + */ + RESERVATION_DATA, +} diff --git a/shongo-controller/pom.xml b/shongo-controller/pom.xml index a3266ef6b..79cf72936 100644 --- a/shongo-controller/pom.xml +++ b/shongo-controller/pom.xml @@ -201,6 +201,13 @@ ical4j-zoneinfo-outlook 1.0.3 + + + + org.projectlombok + lombok + provided + diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/api/rpc/ReservationServiceImpl.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/api/rpc/ReservationServiceImpl.java index a0b4cd879..2aa006225 100644 --- a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/api/rpc/ReservationServiceImpl.java +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/api/rpc/ReservationServiceImpl.java @@ -8,6 +8,7 @@ import cz.cesnet.shongo.controller.api.*; import cz.cesnet.shongo.controller.api.Reservation; import cz.cesnet.shongo.controller.api.Specification; +import cz.cesnet.shongo.controller.api.Tag; import cz.cesnet.shongo.controller.api.request.*; import cz.cesnet.shongo.controller.authorization.Authorization; import cz.cesnet.shongo.controller.authorization.AuthorizationManager; @@ -2041,7 +2042,21 @@ else if (type.equals("RESOURCE")) { reservationRequestSummary.setAllowCache((Boolean) record[25]); } if (record[26] != null) { - reservationRequestSummary.setResourceTags((String) record[26]); + String resourceTags = (String) record[26]; + Arrays.stream(resourceTags.split("\\|")).map(String::trim).map(resourceTag -> { + String[] parts = resourceTag.split(","); + Tag tag = new Tag(); + tag.setId(parts[0]); + tag.setName(parts[1]); + tag.setType(TagType.valueOf(parts[2])); + if (parts.length > 3) { + tag.setData(parts[3]); + } + return tag; + }).forEach(reservationRequestSummary::addResourceTag); + } + if (record[27] != null) { + reservationRequestSummary.setAuxData((String) record[27]); } return reservationRequestSummary; } diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/AbstractReservationRequest.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/AbstractReservationRequest.java index a26ff68e1..630f7407f 100644 --- a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/AbstractReservationRequest.java +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/AbstractReservationRequest.java @@ -1,5 +1,8 @@ package cz.cesnet.shongo.controller.booking.request; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import cz.cesnet.shongo.CommonReportSet; import cz.cesnet.shongo.PersistentObject; import cz.cesnet.shongo.TodoImplementException; @@ -9,9 +12,13 @@ import cz.cesnet.shongo.controller.ObjectType; import cz.cesnet.shongo.controller.ReservationRequestPurpose; import cz.cesnet.shongo.controller.ReservationRequestReusement; +import cz.cesnet.shongo.controller.api.AuxiliaryData; import cz.cesnet.shongo.controller.api.Controller; import cz.cesnet.shongo.controller.booking.Allocation; import cz.cesnet.shongo.controller.booking.ObjectIdentifier; +import cz.cesnet.shongo.controller.booking.request.auxdata.AuxData; +import cz.cesnet.shongo.controller.booking.resource.Resource; +import cz.cesnet.shongo.controller.booking.resource.Tag; import cz.cesnet.shongo.controller.booking.specification.Specification; import cz.cesnet.shongo.controller.scheduler.Scheduler; import cz.cesnet.shongo.controller.api.ReservationRequestType; @@ -20,12 +27,15 @@ import cz.cesnet.shongo.report.Report; import cz.cesnet.shongo.report.ReportableSimple; import cz.cesnet.shongo.util.ObjectHelper; +import org.hibernate.annotations.Type; import org.joda.time.DateTime; import org.joda.time.Period; import javax.persistence.*; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * Represents a base class for all reservation requests which contains common attributes. @@ -37,6 +47,10 @@ @Inheritance(strategy = InheritanceType.JOINED) public abstract class AbstractReservationRequest extends PersistentObject implements ReportableSimple { + + @Transient + private final ObjectMapper objectMapper = new ObjectMapper(); + /** * Date/time when the {@link AbstractReservationRequest} was created. */ @@ -115,6 +129,11 @@ public abstract class AbstractReservationRequest extends PersistentObject implem */ private ReservationRequestReusement reusement; + /** + * Auxiliary data. This data are specified by the {@link Tag}s of {@link Resource} which is requested for reservation. + */ + private String auxData; + @Id @SequenceGenerator(name = "reservation_request_id", sequenceName = "reservation_request_id_seq", allocationSize = 1) @GeneratedValue(strategy = GenerationType.AUTO, generator = "reservation_request_id") @@ -385,6 +404,49 @@ public void setReusement(ReservationRequestReusement reusement) this.reusement = reusement; } + /** + * @return {@link #auxData} + */ + @Type(type = "jsonb") + @Column(name = "aux_data", columnDefinition = "text") + public String getAuxData() + { + return auxData; + } + + /** + * @return {@link #auxData} + */ + @Transient + public List getAuxDataList() throws JsonProcessingException + { + if (auxData == null) { + return null; + } + return objectMapper.readValue(getAuxData(), new TypeReference<>(){}); + } + + /** + * @param auxData sets the {@link #auxData} + */ + public void setAuxData(String auxData) + { + this.auxData = auxData; + } + + /** + * @param auxDataApi sets the {@link #auxData} + */ + public void setAuxData(List auxDataApi) + { + List auxData = auxDataApi.stream().map(AuxData::fromApi).collect(Collectors.toList()); + try { + setAuxData(objectMapper.writeValueAsString(auxData)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + /** * Validate {@link AbstractReservationRequest}. * @@ -448,6 +510,7 @@ public boolean synchronizeFrom(AbstractReservationRequest reservationRequest, En setReusedAllocation(reservationRequest.getReusedAllocation()); setReusedAllocationMandatory(reservationRequest.isReusedAllocationMandatory()); setReusement(reservationRequest.getReusement()); + setAuxData(reservationRequest.getAuxData()); Specification oldSpecification = getSpecification(); Specification newSpecification = reservationRequest.getSpecification(); @@ -550,6 +613,12 @@ protected void toApi(cz.cesnet.shongo.controller.api.AbstractReservationRequest ObjectIdentifier.formatId(reusedAllocation.getReservationRequest()), reusedAllocationMandatory); } api.setReusement(getReusement()); + try { + api.setAuxData(getAuxDataList().stream().map(AuxData::toApi).collect(Collectors.toList())); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } catch (NullPointerException ignored) { + } // Reservation request is deleted if (state.equals(State.DELETED)) { @@ -604,6 +673,7 @@ else if (getSpecification() != null && getSpecification().equalsId(specification } setReusedAllocationMandatory(api.isReusedReservationRequestMandatory()); setReusement(api.getReusement()); + setAuxData(api.getAuxData()); } /** diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/AbstractReservationRequestAuxData.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/AbstractReservationRequestAuxData.java new file mode 100644 index 000000000..92ebe267b --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/AbstractReservationRequestAuxData.java @@ -0,0 +1,30 @@ +package cz.cesnet.shongo.controller.booking.request; + +import com.fasterxml.jackson.databind.JsonNode; +import cz.cesnet.shongo.controller.booking.specification.Specification; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.Immutable; +import org.hibernate.annotations.Type; + +import javax.persistence.*; + +@Getter +@Setter +@Entity +@Immutable +@Table(name = "arr_aux_data") +public class AbstractReservationRequestAuxData +{ + + @Id + private Long id; + + private String tagName; + private Boolean enabled; + @Type(type = "jsonb") + @Column(columnDefinition = "text") + private JsonNode data; + @ManyToOne(cascade = CascadeType.ALL, optional = false, fetch = FetchType.LAZY) + private Specification specification; +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/ReservationRequestManager.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/ReservationRequestManager.java index 1b657cbac..3516db3b0 100644 --- a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/ReservationRequestManager.java +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/ReservationRequestManager.java @@ -1,8 +1,10 @@ package cz.cesnet.shongo.controller.booking.request; +import com.fasterxml.jackson.databind.JsonNode; import cz.cesnet.shongo.AbstractManager; import cz.cesnet.shongo.CommonReportSet; import cz.cesnet.shongo.controller.ControllerReportSetHelper; +import cz.cesnet.shongo.controller.api.TagType; import cz.cesnet.shongo.controller.authorization.AuthorizationManager; import cz.cesnet.shongo.controller.booking.Allocation; import cz.cesnet.shongo.controller.booking.compartment.CompartmentSpecification; @@ -10,6 +12,9 @@ import cz.cesnet.shongo.controller.booking.participant.InvitedPersonParticipant; import cz.cesnet.shongo.controller.booking.participant.AbstractParticipant; import cz.cesnet.shongo.controller.booking.participant.PersonParticipant; +import cz.cesnet.shongo.controller.booking.request.auxdata.AuxDataFilter; +import cz.cesnet.shongo.controller.booking.request.auxdata.AuxDataMerged; +import cz.cesnet.shongo.controller.booking.request.auxdata.tagdata.TagData; import cz.cesnet.shongo.controller.booking.specification.Specification; import cz.cesnet.shongo.controller.booking.reservation.Reservation; import cz.cesnet.shongo.controller.booking.reservation.ReservationManager; @@ -19,7 +24,9 @@ import javax.persistence.EntityManager; import javax.persistence.NoResultException; +import javax.persistence.TypedQuery; import java.util.*; +import java.util.stream.Collectors; /** * Manager for {@link AbstractReservationRequest}. @@ -635,4 +642,74 @@ public List detachReports(ReservationRequest reservationRequest reservationRequest.clearReports(); return reports; } + + /** + * Creates {@link TagData} for given {@link AbstractReservationRequest} and its corresponding + * {@link cz.cesnet.shongo.controller.booking.resource.Tag}s. + * + * @param reservationRequest reservation request for which the {@link TagData} shall be created + * @param filter filter for data desired + * @return specific implementation of {@link TagData} based on {@link TagType} + * @param TagData implementation for corresponding {@link TagType} + */ + public > List getTagData(AbstractReservationRequest reservationRequest, AuxDataFilter filter) + { + return getAuxData(reservationRequest, filter) + .stream() + .map(TagData::create) + .map(data -> (T) data) + .collect(Collectors.toList()); + } + + /** + * Merge {@link cz.cesnet.shongo.controller.booking.request.auxdata.AuxData} from {@link AbstractReservationRequest} + * and data from its corresponding {@link cz.cesnet.shongo.controller.booking.resource.Tag}s. + * + * @param reservationRequest reservation request for which the data shall be merged + * @param filter filter for data desired + * @return merged data + */ + private List getAuxData(AbstractReservationRequest reservationRequest, AuxDataFilter filter) + { + String queryString = "SELECT arr.tagName, rt.tag.type, arr.enabled, arr.data, rt.tag.data" + + " FROM AbstractReservationRequestAuxData arr" + + " JOIN ResourceSpecification res_spec ON res_spec.id = arr.specification.id" + + " JOIN ResourceTag rt ON rt.resource.id = res_spec.resource.id" + + " WHERE rt.tag.name = arr.tagName" + + " AND arr.id = :id"; + if (filter.getTagName() != null) { + queryString += " AND rt.tag.name = :tagName"; + } + if (filter.getTagType() != null) { + queryString += " AND rt.tag.type = :type"; + } + if (filter.getEnabled() != null) { + queryString += " AND arr.enabled = :enabled"; + } + + TypedQuery query = entityManager.createQuery(queryString, Object[].class) + .setParameter("id", reservationRequest.getId()); + if (filter.getTagName() != null) { + query.setParameter("tagName", filter.getTagName()); + } + if (filter.getTagType() != null) { + query.setParameter("type", filter.getTagType()); + } + if (filter.getEnabled() != null) { + query.setParameter("enabled", filter.getEnabled()); + } + + return query + .getResultList() + .stream() + .map(record -> { + final String tagName = (String) record[0]; + final TagType type = (TagType) record[1]; + final Boolean enabled = (Boolean) record[2]; + final JsonNode auxData = (JsonNode) record[3]; + final JsonNode data = (JsonNode) record[4]; + return new AuxDataMerged(tagName, type, enabled, data, auxData); + }) + .collect(Collectors.toList()); + } } diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/AuxData.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/AuxData.java new file mode 100644 index 000000000..fcc352427 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/AuxData.java @@ -0,0 +1,31 @@ +package cz.cesnet.shongo.controller.booking.request.auxdata; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; + +@Data +public class AuxData +{ + + private String tagName; + private boolean enabled; + private JsonNode data; + + public cz.cesnet.shongo.controller.api.AuxiliaryData toApi() + { + cz.cesnet.shongo.controller.api.AuxiliaryData api = new cz.cesnet.shongo.controller.api.AuxiliaryData(); + api.setTagName(getTagName()); + api.setEnabled(isEnabled()); + api.setData(getData()); + return api; + } + + public static AuxData fromApi(cz.cesnet.shongo.controller.api.AuxiliaryData api) + { + AuxData auxData = new AuxData(); + auxData.setTagName(api.getTagName()); + auxData.setEnabled(api.isEnabled()); + auxData.setData(api.getDataAsJsonNode()); + return auxData; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/AuxDataFilter.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/AuxDataFilter.java new file mode 100644 index 000000000..9c03fea12 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/AuxDataFilter.java @@ -0,0 +1,15 @@ +package cz.cesnet.shongo.controller.booking.request.auxdata; + +import cz.cesnet.shongo.controller.api.TagType; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class AuxDataFilter +{ + + private final String tagName; + private final TagType tagType; + private final Boolean enabled; +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/AuxDataMerged.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/AuxDataMerged.java new file mode 100644 index 000000000..d501d2bf5 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/AuxDataMerged.java @@ -0,0 +1,18 @@ +package cz.cesnet.shongo.controller.booking.request.auxdata; + +import com.fasterxml.jackson.databind.JsonNode; +import cz.cesnet.shongo.controller.api.TagType; +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor +public class AuxDataMerged +{ + + String tagName; + TagType type; + Boolean enabled; + JsonNode data; + JsonNode auxData; +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/tagdata/DefaultAuxData.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/tagdata/DefaultAuxData.java new file mode 100644 index 000000000..59504529d --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/tagdata/DefaultAuxData.java @@ -0,0 +1,18 @@ +package cz.cesnet.shongo.controller.booking.request.auxdata.tagdata; + +import cz.cesnet.shongo.controller.booking.request.auxdata.AuxDataMerged; + +public class DefaultAuxData extends TagData +{ + + public DefaultAuxData(AuxDataMerged auxData) + { + super(auxData); + } + + @Override + protected Void constructData() + { + return null; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/tagdata/NotifyEmailAuxData.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/tagdata/NotifyEmailAuxData.java new file mode 100644 index 000000000..d59d7623f --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/tagdata/NotifyEmailAuxData.java @@ -0,0 +1,62 @@ +package cz.cesnet.shongo.controller.booking.request.auxdata.tagdata; + +import com.fasterxml.jackson.databind.JsonNode; +import cz.cesnet.shongo.controller.api.TagType; +import cz.cesnet.shongo.controller.booking.request.auxdata.AuxDataMerged; +import lombok.extern.slf4j.Slf4j; + +import javax.mail.internet.AddressException; +import javax.mail.internet.InternetAddress; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +public class NotifyEmailAuxData extends TagData> +{ + + public NotifyEmailAuxData(AuxDataMerged auxData) + { + super(auxData); + if (!TagType.NOTIFY_EMAIL.equals(auxData.getType())) { + throw new IllegalArgumentException("AuxData is not of type NOTIFY_EMAIL"); + } + } + + @Override + protected List constructData() + { + if (!auxData.getData().isArray()) { + throw new IllegalArgumentException("Tag data is not an array"); + } + if (!auxData.getAuxData().isArray()) { + throw new IllegalArgumentException("AuxData data is not an array"); + } + + List emails = new ArrayList<>(); + + for (JsonNode child : auxData.getAuxData()) { + emails.add(child.asText()); + } + for (JsonNode child : auxData.getData()) { + emails.add(child.asText()); + } + emails.forEach(email -> { + if (!isValidEmailAddress(email)) { + throw new IllegalArgumentException("Invalid email address: " + email); + } + }); + + return emails; + } + + public static boolean isValidEmailAddress(String email) { + try { + InternetAddress emailAddr = new InternetAddress(email); + emailAddr.validate(); + } catch (AddressException ex) { + log.info("Invalid email address: " + email, ex); + return false; + } + return true; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/tagdata/ReservationAuxData.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/tagdata/ReservationAuxData.java new file mode 100644 index 000000000..f5483db41 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/tagdata/ReservationAuxData.java @@ -0,0 +1,29 @@ +package cz.cesnet.shongo.controller.booking.request.auxdata.tagdata; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import cz.cesnet.shongo.controller.api.TagType; +import cz.cesnet.shongo.controller.booking.request.auxdata.AuxDataMerged; + +import java.util.Map; + +public class ReservationAuxData extends TagData> +{ + + public ReservationAuxData(AuxDataMerged auxData) + { + super(auxData); + if (!TagType.RESERVATION_DATA.equals(auxData.getType())) { + throw new IllegalArgumentException("AuxData is not of type RESERVATION_DATA"); + } + } + + @Override + protected Map constructData() + { + Map tagMap = new ObjectMapper().convertValue(auxData.getData(), new TypeReference<>() {}); + Map auxMap = new ObjectMapper().convertValue(auxData.getAuxData(), new TypeReference<>() {}); + tagMap.putAll(auxMap); + return tagMap; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/tagdata/TagData.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/tagdata/TagData.java new file mode 100644 index 000000000..01744c8f0 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/tagdata/TagData.java @@ -0,0 +1,58 @@ +package cz.cesnet.shongo.controller.booking.request.auxdata.tagdata; + +import cz.cesnet.shongo.TodoImplementException; +import cz.cesnet.shongo.controller.booking.request.auxdata.AuxDataFilter; +import cz.cesnet.shongo.controller.booking.request.auxdata.AuxDataMerged; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public abstract class TagData +{ + + protected final AuxDataMerged auxData; + protected final T data; + + protected TagData(AuxDataMerged auxData) + { + this.auxData = auxData; + this.data = constructData(); + } + + protected abstract T constructData(); + + public static TagData create(AuxDataMerged auxData) + { + switch (auxData.getType()) { + case DEFAULT: + return new DefaultAuxData(auxData); + case NOTIFY_EMAIL: + return new NotifyEmailAuxData(auxData); + case RESERVATION_DATA: + return new ReservationAuxData(auxData); + default: + throw new TodoImplementException("Not implemented for tag type: " + auxData.getType()); + } + } + + public boolean filter(AuxDataFilter filter) + { + if (filter.getTagName() != null) { + if (!filter.getTagName().equals(auxData.getTagName())) { + return false; + } + } + if (filter.getTagType() != null) { + if (!filter.getTagType().equals(auxData.getType())) { + return false; + } + } + if (filter.getEnabled() != null) { + if (!filter.getEnabled().equals(auxData.getEnabled())) { + return false; + } + } + return true; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/resource/ResourceManager.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/resource/ResourceManager.java index bcab61e34..6e3573745 100644 --- a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/resource/ResourceManager.java +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/resource/ResourceManager.java @@ -27,6 +27,7 @@ import javax.persistence.TypedQuery; import javax.persistence.criteria.*; import java.util.List; +import java.util.stream.Collectors; /** * Manager for {@link Resource}. @@ -686,6 +687,26 @@ public List getForeignResourceTags(ForeignResources foreignResource return typedQuery.getResultList(); } + /** + * Returns list of {@link Tag} for given {@link Resource} + * @param resource + * @return + */ + public List getResourceTags(Resource resource) + { + CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); + + CriteriaQuery query = criteriaBuilder.createQuery(ResourceTag.class); + Root resourceTagRoot = query.from(ResourceTag.class); + javax.persistence.criteria.Predicate param1 = criteriaBuilder.equal(resourceTagRoot.get("resource"), resource.getId()); + query.select(resourceTagRoot).where(param1); + + TypedQuery typedQuery = entityManager.createQuery(query); + List resourceTags = typedQuery.getResultList(); + + return resourceTags.stream().map(ResourceTag::getTag).collect(Collectors.toList()); + } + /** * List {@link ResourceTag} by given {@link Tag} id * @param tagId diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/resource/Tag.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/resource/Tag.java index f3243b416..6df57e70d 100644 --- a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/resource/Tag.java +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/resource/Tag.java @@ -1,28 +1,71 @@ package cz.cesnet.shongo.controller.booking.resource; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import cz.cesnet.shongo.SimplePersistentObject; import cz.cesnet.shongo.api.AbstractComplexType; +import cz.cesnet.shongo.controller.api.TagType; import cz.cesnet.shongo.controller.booking.ObjectIdentifier; +import org.hibernate.annotations.Type; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Transient; /** * @author: Ondřej Pavelka */ @Entity public class Tag extends SimplePersistentObject { + + @Transient + private final ObjectMapper objectMapper = new ObjectMapper(); + private String name; + private TagType type; + + private JsonNode data; + @Column(length = AbstractComplexType.DEFAULT_COLUMN_LENGTH, unique = true) - public String getName() { + public String getName() + { return name; } - public void setName(String name) { + public void setName(String name) + { this.name = name; } + @Column(nullable = false, length = AbstractComplexType.ENUM_COLUMN_LENGTH) + @Enumerated(EnumType.STRING) + public TagType getType() + { + return type; + } + + public void setType(TagType type) + { + this.type = type; + } + + // @Type and @Column both needed, because HSQLDB does not support JSONB type + @Type(type = "jsonb") + @Column(columnDefinition = "text") + public JsonNode getData() + { + return data; + } + + public void setData(JsonNode data) + { + this.data = data; + } + /** * @return tag converted capability to API */ @@ -37,6 +80,17 @@ public void toApi(cz.cesnet.shongo.controller.api.Tag tagApi) { tagApi.setId(ObjectIdentifier.formatId(this)); tagApi.setName(name); + tagApi.setType(type); + if (data == null) { + tagApi.setData(""); + } + else { + try { + tagApi.setData(objectMapper.writeValueAsString(data)); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Failed to parse data", e); + } + } } /** @@ -53,6 +107,13 @@ public static Tag createFromApi(cz.cesnet.shongo.controller.api.Tag tagApi) public void fromApi(cz.cesnet.shongo.controller.api.Tag tagApi) { this.setName(tagApi.getName()); + this.setType(tagApi.getType()); + if (tagApi.getData() != null) { + try { + setData(objectMapper.readTree(tagApi.getData())); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Data is not a valid JSON", e); + } + } } - } diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/notification/ReservationNotification.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/notification/ReservationNotification.java index d04a9363d..44096943a 100644 --- a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/notification/ReservationNotification.java +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/notification/ReservationNotification.java @@ -7,11 +7,15 @@ import cz.cesnet.shongo.api.UserInformation; import cz.cesnet.shongo.controller.LocalDomain; import cz.cesnet.shongo.controller.ObjectRole; +import cz.cesnet.shongo.controller.api.TagType; import cz.cesnet.shongo.controller.authorization.AuthorizationManager; import cz.cesnet.shongo.controller.booking.Allocation; import cz.cesnet.shongo.controller.booking.ObjectIdentifier; import cz.cesnet.shongo.controller.booking.alias.Alias; import cz.cesnet.shongo.controller.booking.request.AbstractReservationRequest; +import cz.cesnet.shongo.controller.booking.request.ReservationRequestManager; +import cz.cesnet.shongo.controller.booking.request.auxdata.AuxDataFilter; +import cz.cesnet.shongo.controller.booking.request.auxdata.tagdata.NotifyEmailAuxData; import cz.cesnet.shongo.controller.booking.reservation.Reservation; import cz.cesnet.shongo.controller.booking.resource.Resource; import cz.cesnet.shongo.controller.booking.room.RoomEndpoint; @@ -22,6 +26,7 @@ import javax.persistence.EntityManager; import java.util.*; +import java.util.stream.Collectors; /** * {@link ConfigurableNotification} for a {@link Reservation}. @@ -45,7 +50,9 @@ public abstract class ReservationNotification extends AbstractReservationRequest private Map childTargetByReservation = new LinkedHashMap(); private ReservationNotification(Reservation reservation, - AbstractReservationRequest reservationRequest, AuthorizationManager authorizationManager) + AbstractReservationRequest reservationRequest, + AuthorizationManager authorizationManager, + ReservationRequestManager reservationRequestManager) { super(reservationRequest); @@ -63,6 +70,7 @@ private ReservationNotification(Reservation reservation, // Add administrators as recipients addAdministratorRecipientsForReservation(reservation.getTargetReservation(), authorizationManager); + addRecipientsFromNotificationTags(reservationRequest, reservationRequestManager); // Add child targets for (Reservation childReservation : reservation.getChildReservations()) { @@ -70,6 +78,34 @@ private ReservationNotification(Reservation reservation, } } + private void addRecipientsFromNotificationTags(AbstractReservationRequest reservationRequest, + ReservationRequestManager reservationRequestManager) + { + AuxDataFilter filter = AuxDataFilter.builder() + .tagType(TagType.NOTIFY_EMAIL) + .enabled(true) + .build(); + + List notifyEmailAuxData = reservationRequestManager.getTagData(reservationRequest, filter); + + List tagPersonInformationList = notifyEmailAuxData + .stream() + .map(this::notifyEmailDataToPersonInformation) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + logger.debug("Adding tag recipients: {}", tagPersonInformationList); + tagPersonInformationList.forEach(personInformation -> addRecipient(personInformation, true)); + } + + private Collection notifyEmailDataToPersonInformation(NotifyEmailAuxData notifyEmailAuxData) + { + return notifyEmailAuxData + .getData() + .stream() + .map(email -> new TagPersonInformation(notifyEmailAuxData.getAuxData().getTagName(), email)) + .collect(Collectors.toList()); + } + public String getId() { return id; @@ -339,9 +375,10 @@ public static class New extends ReservationNotification { private Long previousReservationId; - public New(Reservation reservation, Reservation previousReservation, AuthorizationManager authorizationManager) + public New(Reservation reservation, Reservation previousReservation, AuthorizationManager authorizationManager, + ReservationRequestManager reservationRequestManager) { - super(reservation, getReservationRequest(reservation), authorizationManager); + super(reservation, getReservationRequest(reservation), authorizationManager, reservationRequestManager); this.previousReservationId = (previousReservation != null ? previousReservation.getId() : null); } @@ -373,14 +410,15 @@ protected String getTitleReservationId(RenderContext renderContext) public static class Deleted extends ReservationNotification { public Deleted(Reservation reservation, AbstractReservationRequest reservationRequest, - AuthorizationManager authorizationManager) + AuthorizationManager authorizationManager, ReservationRequestManager reservationRequestManager) { - super(reservation, reservationRequest, authorizationManager); + super(reservation, reservationRequest, authorizationManager, reservationRequestManager); } - public Deleted(Reservation reservation, AuthorizationManager authorizationManager) + public Deleted(Reservation reservation, AuthorizationManager authorizationManager, + ReservationRequestManager reservationRequestManager) { - super(reservation, getReservationRequest(reservation), authorizationManager); + super(reservation, getReservationRequest(reservation), authorizationManager, reservationRequestManager); } @Override @@ -389,4 +427,41 @@ public String getType() return "DELETED"; } } + + private static class TagPersonInformation implements PersonInformation + { + + private final String name; + private final String email; + + public TagPersonInformation(String name, String email) + { + this.name = name; + this.email = email; + } + + @Override + public String getFullName() + { + return name; + } + + @Override + public String getRootOrganization() + { + return null; + } + + @Override + public String getPrimaryEmail() + { + return email; + } + + @Override + public String toString() + { + return "Tag[" + name + "] (" + email + ")"; + } + } } diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/scheduler/Scheduler.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/scheduler/Scheduler.java index 54b95b6d9..e5995e98d 100644 --- a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/scheduler/Scheduler.java +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/scheduler/Scheduler.java @@ -590,7 +590,7 @@ private void allocateReservationRequest(ReservationRequest reservationRequest, S // Create notification contextState.addNotification(new ReservationNotification.New( - allocatedReservation, previousReservation, authorizationManager)); + allocatedReservation, previousReservation, authorizationManager, reservationRequestManager)); // Update reservation request if (context.getRequestWantedState() != null) { diff --git a/shongo-controller/src/main/resources/sql/hsqldb/init.sql b/shongo-controller/src/main/resources/sql/hsqldb/init.sql index 861e651c5..a0b50e5cf 100644 --- a/shongo-controller/src/main/resources/sql/hsqldb/init.sql +++ b/shongo-controller/src/main/resources/sql/hsqldb/init.sql @@ -30,7 +30,7 @@ SELECT WHEN (SELECT resource_id FROM capability INNER JOIN recording_capability on recording_capability.id = capability.id WHERE resource_id = resource.id) IS NOT NULL THEN 'RECORDING_SERVICE' ELSE 'RESOURCE' END AS type, - GROUP_CONCAT(tag.name SEPARATOR ',') AS tag_names + GROUP_CONCAT(CONCAT(tag.id, ',', tag.name, ',', tag.type, ',', tag.data) SEPARATOR '|') AS tags FROM resource LEFT JOIN device_resource ON device_resource.id = resource.id LEFT JOIN device_resource_technologies ON device_resource_technologies.device_resource_id = device_resource.id @@ -97,6 +97,7 @@ SELECT reused_allocation.abstract_reservation_request_id AS reused_reservation_request_id, abstract_reservation_request.modified_reservation_request_id AS modified_reservation_request_id, abstract_reservation_request.allocation_id AS allocation_id, + abstract_reservation_request.aux_data AS aux_data, NULL AS child_id, NULL AS future_child_count, reservation_request.slot_start AS slot_start, diff --git a/shongo-controller/src/main/resources/sql/postgresql/init.sql b/shongo-controller/src/main/resources/sql/postgresql/init.sql index b219e9a29..4b120d52c 100644 --- a/shongo-controller/src/main/resources/sql/postgresql/init.sql +++ b/shongo-controller/src/main/resources/sql/postgresql/init.sql @@ -13,6 +13,7 @@ DROP VIEW IF EXISTS reservation_request_earliest_usage; DROP VIEW IF EXISTS reservation_summary; DROP VIEW IF EXISTS executable_summary_view; DROP VIEW IF EXISTS room_endpoint_earliest_usage; +DROP VIEW IF EXISTS arr_aux_data; /** * Create missing foreign keys' indexes. @@ -112,7 +113,7 @@ SELECT WHEN resource.id IN (SELECT resource_id FROM capability INNER JOIN recording_capability on recording_capability.id = capability.id) THEN 'RECORDING_SERVICE' ELSE 'RESOURCE' END AS type, - string_agg(tag.name, ',') AS tag_names + string_agg(tag.id || ',' || tag.name || ',' || tag.type || ',' || COALESCE(tag.data #>> '{}', ''), '|') AS tags FROM resource LEFT JOIN device_resource ON device_resource.id = resource.id LEFT JOIN device_resource_technologies ON device_resource_technologies.device_resource_id = device_resource.id @@ -340,6 +341,7 @@ FROM ( reused_allocation.abstract_reservation_request_id AS reused_reservation_request_id, abstract_reservation_request.modified_reservation_request_id AS modified_reservation_request_id, abstract_reservation_request.allocation_id AS allocation_id, + abstract_reservation_request.aux_data #>> '{}' AS aux_data, reservation_request_set_earliest_child.child_id AS child_id, reservation_request_set_earliest_child.future_child_count AS future_child_count, COALESCE(reservation_request.slot_start, reservation_request_set_earliest_child.slot_start) AS slot_start, @@ -540,3 +542,11 @@ ORDER BY executable.id, alias.id; CREATE TABLE executable_summary AS SELECT * FROM executable_summary_view; CREATE TABLE specification_summary AS SELECT * FROM specification_summary_view; + +CREATE VIEW arr_aux_data AS +SELECT + arr.*, + jsonb_array_elements(aux_data)->>'tagName' AS tag_name, + (jsonb_array_elements(aux_data)->>'enabled')::boolean AS enabled, + jsonb_array_elements(aux_data)->'data' AS data +FROM abstract_reservation_request arr; diff --git a/shongo-controller/src/main/resources/sql/reservation_request_list.sql b/shongo-controller/src/main/resources/sql/reservation_request_list.sql index dcd6a76dd..60ecd03c9 100644 --- a/shongo-controller/src/main/resources/sql/reservation_request_list.sql +++ b/shongo-controller/src/main/resources/sql/reservation_request_list.sql @@ -37,7 +37,8 @@ SELECT foreign_resources.foreign_resource_id, domain.name as domain_name, reservation_request_summary.allowCache as allowCache, - resource_summary.tag_names as tag_names + resource_summary.tags as tags, + reservation_request_summary.aux_data as aux_data FROM reservation_request_summary LEFT JOIN reservation_request ON reservation_request.id = reservation_request_summary.id LEFT JOIN specification_summary ON specification_summary.id = reservation_request_summary.specification_id diff --git a/shongo-controller/src/test/java/cz/cesnet/shongo/controller/AbstractDatabaseTest.java b/shongo-controller/src/test/java/cz/cesnet/shongo/controller/AbstractDatabaseTest.java index d9dde912d..56470c926 100644 --- a/shongo-controller/src/test/java/cz/cesnet/shongo/controller/AbstractDatabaseTest.java +++ b/shongo-controller/src/test/java/cz/cesnet/shongo/controller/AbstractDatabaseTest.java @@ -24,7 +24,7 @@ public abstract class AbstractDatabaseTest * Connection. */ protected static String connectionDriver = "org.hsqldb.jdbcDriver"; - protected static String connectionUrl = "jdbc:hsqldb:mem:test; shutdown=true;"; + protected static String connectionUrl = "jdbc:hsqldb:mem:test; shutdown=true; sql.syntax_pgs=true;"; /** * Enable driver for debugging SQL. diff --git a/shongo-controller/src/test/java/cz/cesnet/shongo/controller/booking/request/ReservationRequestModificationTest.java b/shongo-controller/src/test/java/cz/cesnet/shongo/controller/booking/request/ReservationRequestModificationTest.java index f58598701..d766573e9 100644 --- a/shongo-controller/src/test/java/cz/cesnet/shongo/controller/booking/request/ReservationRequestModificationTest.java +++ b/shongo-controller/src/test/java/cz/cesnet/shongo/controller/booking/request/ReservationRequestModificationTest.java @@ -8,17 +8,14 @@ import cz.cesnet.shongo.controller.ReservationRequestPurpose; import cz.cesnet.shongo.controller.ReservationRequestReusement; import cz.cesnet.shongo.controller.api.*; -import cz.cesnet.shongo.controller.api.AbstractReservationRequest; +import cz.cesnet.shongo.controller.api.AuxiliaryData; import cz.cesnet.shongo.controller.api.ReservationRequest; -import cz.cesnet.shongo.controller.api.ReservationRequestSet; import cz.cesnet.shongo.controller.api.rpc.ReservationService; -import cz.cesnet.shongo.controller.booking.datetime.AbsoluteDateTimeSlot; import org.joda.time.Interval; import org.junit.Assert; import org.junit.Test; import java.util.List; -import java.util.Locale; /** * Tests for reallocation of reservations. @@ -27,6 +24,74 @@ */ public class ReservationRequestModificationTest extends AbstractControllerTest { + + @Test + public void testModifyAttributes() { + Resource resource = new Resource(); + resource.setName("resource"); + resource.setAllocatable(true); + String resourceId = createResource(resource); + + ReservationRequest reservationRequest = new ReservationRequest(); + reservationRequest.setDescription("request"); + reservationRequest.setSlot("2012-01-01T12:00", "PT2H"); + reservationRequest.setPurpose(ReservationRequestPurpose.SCIENCE); + reservationRequest.setSpecification(new ResourceSpecification(resourceId)); + + String id1 = getReservationService().createReservationRequest(SECURITY_TOKEN, reservationRequest); + ReservationRequest reservationRequestGet = getReservationRequest(id1, ReservationRequest.class); + + Assert.assertEquals(ReservationRequestType.NEW, reservationRequestGet.getType()); + Assert.assertEquals(reservationRequest.getPurpose(), reservationRequestGet.getPurpose()); + Assert.assertEquals(reservationRequest.getDescription(), reservationRequestGet.getDescription()); + Assert.assertEquals(reservationRequest.getInterDomain(), reservationRequestGet.getInterDomain()); + Assert.assertEquals(reservationRequest.getReusedReservationRequestId(), reservationRequestGet.getReusedReservationRequestId()); + Assert.assertEquals(ReservationRequestReusement.NONE, reservationRequestGet.getReusement()); + Assert.assertEquals(reservationRequest.getAuxData(), reservationRequestGet.getAuxData()); + Assert.assertEquals(reservationRequest.getParentReservationRequestId(), reservationRequestGet.getParentReservationRequestId()); + Assert.assertEquals(reservationRequest.getSlot(), reservationRequestGet.getSlot()); + Assert.assertEquals(reservationRequest.getReservationIds(), reservationRequestGet.getReservationIds()); + + // Modify reservation request by retrieved instance of reservation request + reservationRequestGet.setPurpose(ReservationRequestPurpose.EDUCATION); + reservationRequestGet.setPriority(5); + reservationRequestGet.setDescription("requestModified"); + reservationRequestGet.setSpecification(new AliasSpecification(Technology.ADOBE_CONNECT)); + reservationRequestGet.setReusement(ReservationRequestReusement.OWNED); + List auxData = List.of( + new AuxiliaryData("tag1", true, "[\"karnis@cenet.cz\", \"filip.karnis@cesnet.cz\"]"), + new AuxiliaryData("tag2", false, "[\"shouldnotbe@used\"]"), + new AuxiliaryData("tag3", true, null) + ); + reservationRequestGet.setAuxData(auxData); + reservationRequestGet.setSlot("2012-01-01T13:00", "PT1H"); + + String id2 = getReservationService().modifyReservationRequest(SECURITY_TOKEN, reservationRequestGet); + ReservationRequest reservationRequestGet2 = getReservationRequest(id2, ReservationRequest.class); + + Assert.assertEquals(ReservationRequestType.MODIFIED, reservationRequestGet2.getType()); + Assert.assertEquals(reservationRequestGet.getPurpose(), reservationRequestGet2.getPurpose()); + Assert.assertEquals(reservationRequestGet.getPriority(), reservationRequestGet2.getPriority()); + Assert.assertEquals(reservationRequestGet.getDescription(), reservationRequestGet2.getDescription()); + Assert.assertEquals(reservationRequestGet.getInterDomain(), reservationRequestGet2.getInterDomain()); + Assert.assertEquals(reservationRequestGet.getReusedReservationRequestId(), reservationRequestGet2.getReusedReservationRequestId()); + Assert.assertEquals(reservationRequestGet.getReusement(), reservationRequestGet2.getReusement()); + Assert.assertEquals(reservationRequestGet.getAuxData(), reservationRequestGet2.getAuxData()); + Assert.assertEquals(reservationRequestGet.getParentReservationRequestId(), reservationRequestGet2.getParentReservationRequestId()); + Assert.assertEquals(reservationRequestGet.getSlot(), reservationRequestGet2.getSlot()); + Assert.assertEquals(reservationRequestGet.getAllocationState(), reservationRequestGet2.getAllocationState()); + Assert.assertEquals(reservationRequestGet.getReservationIds(), reservationRequestGet2.getReservationIds()); + + // Modify again + reservationRequestGet2.setReusedReservationRequestId(id2); + + String id3 = getReservationService().modifyReservationRequest(SECURITY_TOKEN, reservationRequestGet2); + ReservationRequest reservationRequestGet3 = getReservationRequest(id3, ReservationRequest.class); + + // Check that reused reservation request points to id3 since id2 was modified to id3 + Assert.assertEquals(id3, reservationRequestGet3.getReusedReservationRequestId()); + } + @Test public void testExtension() throws Exception { diff --git a/shongo-controller/src/test/java/cz/cesnet/shongo/controller/booking/resource/TagTest.java b/shongo-controller/src/test/java/cz/cesnet/shongo/controller/booking/resource/TagTest.java index 788125d87..55dd073a2 100644 --- a/shongo-controller/src/test/java/cz/cesnet/shongo/controller/booking/resource/TagTest.java +++ b/shongo-controller/src/test/java/cz/cesnet/shongo/controller/booking/resource/TagTest.java @@ -21,6 +21,47 @@ * @author: Ondřej Pavelka */ public class TagTest extends AbstractControllerTest { + + @Test + public void testCreateTag() + { + final String tagName1 = "testTag1"; + final String tagName2 = "testTag2"; + final TagType tagType2 = TagType.NOTIFY_EMAIL; + final String tagData2 = "[\"karnis@cesnet.cz\",\"filip.karnis@cesnet.cz\"]"; + + ResourceService resourceService = getResourceService(); + + // tag1 init + cz.cesnet.shongo.controller.api.Tag tag1 = new cz.cesnet.shongo.controller.api.Tag(); + tag1.setName(tagName1); + + // tag2 init + cz.cesnet.shongo.controller.api.Tag tag2 = new cz.cesnet.shongo.controller.api.Tag(); + tag2.setName(tagName2); + tag2.setType(tagType2); + tag2.setData(tagData2); + + String tagId1 = resourceService.createTag(SECURITY_TOKEN_ROOT, tag1); + String tagId2 = resourceService.createTag(SECURITY_TOKEN_ROOT, tag2); + + cz.cesnet.shongo.controller.api.Tag getResult1 = resourceService.getTag(SECURITY_TOKEN_ROOT, tagId1); + cz.cesnet.shongo.controller.api.Tag getResult2 = resourceService.getTag(SECURITY_TOKEN_ROOT, tagId2); + + Assert.assertNotNull(getResult1); + Assert.assertNotNull(getResult2); + + Assert.assertEquals(tagId1, getResult1.getId()); + Assert.assertEquals(tagName1, getResult1.getName()); + Assert.assertEquals(TagType.DEFAULT, getResult1.getType()); + Assert.assertEquals("", getResult1.getData()); + + Assert.assertEquals(tagId2, getResult2.getId()); + Assert.assertEquals(tagName2, getResult2.getName()); + Assert.assertEquals(tagType2, getResult2.getType()); + Assert.assertEquals(tagData2, getResult2.getData()); + } + @Test public void testCreateTagsAcl() throws Exception { diff --git a/shongo-deployment/migrations/2023-02-15/migration.sql b/shongo-deployment/migrations/2023-02-15/migration.sql new file mode 100644 index 000000000..40825533c --- /dev/null +++ b/shongo-deployment/migrations/2023-02-15/migration.sql @@ -0,0 +1,10 @@ +/** + * 2023-02-15: tag can now hold additional data + */ +BEGIN TRANSACTION; + +ALTER TABLE tag + ADD COLUMN type varchar(255) NOT NULL DEFAULT 'DEFAULT', + ADD COLUMN data jsonb; + +COMMIT TRANSACTION; diff --git a/shongo-deployment/migrations/2023-02-24/migration.sql b/shongo-deployment/migrations/2023-02-24/migration.sql new file mode 100644 index 000000000..8f97bec87 --- /dev/null +++ b/shongo-deployment/migrations/2023-02-24/migration.sql @@ -0,0 +1,17 @@ +/** + * 2023-02-24: add auxData to reservation request + */ +BEGIN TRANSACTION; + +ALTER TABLE abstract_reservation_request ADD COLUMN aux_data jsonb DEFAULT '[]'::jsonb NOT NULL; + +-- Has to be here, otherwise JPA creates TABLE instead of VIEW +CREATE VIEW arr_aux_data AS +SELECT + arr.*, + jsonb_array_elements(aux_data)->>'tagName' AS tag_name, + (jsonb_array_elements(aux_data)->>'enabled')::boolean AS enabled, + jsonb_array_elements(aux_data)->'data' AS data +FROM abstract_reservation_request arr; + +COMMIT TRANSACTION;