diff --git a/.gitignore b/.gitignore index 8c01860ae..c6b5a249d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,6 @@ target/ *.fsps *.class .gitattributes +data/ shongo-deployment/**/*.cfg - 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..06ca9c41b 100644 --- a/shongo-client-cli/src/main/perl/Shongo/ClientCli/ReservationService.pm +++ b/shongo-client-cli/src/main/perl/Shongo/ClientCli/ReservationService.pm @@ -78,7 +78,7 @@ sub populate() 'list-reservation-requests' => { desc => 'List summary of all existing reservation requests', options => 'technology=s search=s resource=s', - args => '[-technology ][-search ][-resource ]', + args => '[-technology ][-search ][-resource ]', method => sub { my ($shell, $params, @args) = @_; list_reservation_requests($params->{'options'}); @@ -251,7 +251,11 @@ sub list_reservation_requests() } } if ( defined($options->{'resource'}) ) { - $request->{'specificationResourceId'} = $options->{'resource'}; + $request->{'specificationResourceIds'} = []; + foreach my $resource (split(/,/, $options->{'resource'})) { + $resource =~ s/(^ +)|( +$)//g; + push(@{$request->{'specificationResourceIds'}}, $resource); + } } my $application = Shongo::ClientCli->instance(); my $response = $application->secure_hash_request('Reservation.listReservationRequests', $request); @@ -274,7 +278,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 +326,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/controllers/ResourceController.java b/shongo-client-web/src/main/java/cz/cesnet/shongo/client/web/controllers/ResourceController.java index 336f144e9..cd18e9bd0 100644 --- a/shongo-client-web/src/main/java/cz/cesnet/shongo/client/web/controllers/ResourceController.java +++ b/shongo-client-web/src/main/java/cz/cesnet/shongo/client/web/controllers/ResourceController.java @@ -283,7 +283,7 @@ public Map handleReservationRequestsConfirmationData( Interval interval = Temporal.roundIntervalToDays(new Interval(intervalFrom, intervalTo)); requestListRequest.setInterval(interval); if (resourceId != null) { - requestListRequest.setSpecificationResourceId(resourceId); + requestListRequest.setSpecificationResourceIds(new HashSet<>(Collections.singleton(resourceId))); } else { throw new TodoImplementException("list request for confirmation generaly"); } 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-api/pom.xml b/shongo-common-api/pom.xml index 5df700ca2..590d779f8 100644 --- a/shongo-common-api/pom.xml +++ b/shongo-common-api/pom.xml @@ -26,7 +26,7 @@ joda-time joda-time - 2.3 + 2.9.9 diff --git a/shongo-common-api/src/main/java/cz/cesnet/shongo/api/Converter.java b/shongo-common-api/src/main/java/cz/cesnet/shongo/api/Converter.java index a8a26a9ea..e25a235df 100644 --- a/shongo-common-api/src/main/java/cz/cesnet/shongo/api/Converter.java +++ b/shongo-common-api/src/main/java/cz/cesnet/shongo/api/Converter.java @@ -1,5 +1,8 @@ package cz.cesnet.shongo.api; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import cz.cesnet.shongo.CommonReportSet; import cz.cesnet.shongo.Temporal; import cz.cesnet.shongo.TodoImplementException; @@ -35,6 +38,8 @@ public class Converter private static final DateTimeFormatter DATE_TIME_FORMATTER = ISODateTimeFormat.dateTimeParser(); + private static final ObjectMapper objectMapper = new ObjectMapper(); + /** * Convert given {@code value} to {@link String}. * @@ -64,6 +69,9 @@ else if (value instanceof Period ) { else if (value instanceof Interval ) { return convertIntervalToString((Interval) value); } + else if (value instanceof JsonNode) { + return convertJsonNodeToString((JsonNode) value); + } else { throw new TodoImplementException(value.getClass()); } @@ -776,6 +784,31 @@ public static List convertToList(Object value, Class componentClass) return list; } + public static String convertJsonNodeToString(JsonNode jsonNode) + { + if (jsonNode == null) { + return ""; + } else { + try { + return objectMapper.writeValueAsString(jsonNode); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + } + + public static JsonNode convertToJsonNode(String value) + { + if (value.isEmpty()) { + return null; + } + try { + return objectMapper.readTree(value); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + /** * Convert given {@code value} to {@link List} value with items of any of given {@code componentClasses}. * diff --git a/shongo-common-api/src/main/java/cz/cesnet/shongo/api/DataMap.java b/shongo-common-api/src/main/java/cz/cesnet/shongo/api/DataMap.java index 612f96732..c32adbae5 100644 --- a/shongo-common-api/src/main/java/cz/cesnet/shongo/api/DataMap.java +++ b/shongo-common-api/src/main/java/cz/cesnet/shongo/api/DataMap.java @@ -1,5 +1,6 @@ package cz.cesnet.shongo.api; +import com.fasterxml.jackson.databind.JsonNode; import cz.cesnet.shongo.CommonReportSet; import cz.cesnet.shongo.TodoImplementException; import org.joda.time.*; @@ -127,6 +128,11 @@ public void set(String property, ReadablePartial readablePartial) setNotNull(property, Converter.convertReadablePartialToString(readablePartial)); } + public void set(String property, JsonNode jsonNode) + { + setNotNull(property, Converter.convertJsonNodeToString(jsonNode)); + } + public void set(String property, Collection collection) { setNotNull(property, collection); @@ -338,6 +344,11 @@ public ReadablePartial getReadablePartial(String property) return Converter.convertToReadablePartial(data.get(property)); } + public JsonNode getJsonNode(String property) + { + return Converter.convertToJsonNode(getString(property)); + } + public List getList(String property, Class componentClass) { return Converter.convertToList(data.get(property), componentClass); diff --git a/shongo-common-api/src/main/java/jade/content/onto/CustomBeanOntologyBuilder.java b/shongo-common-api/src/main/java/jade/content/onto/CustomBeanOntologyBuilder.java index f6c48abab..25edd133a 100644 --- a/shongo-common-api/src/main/java/jade/content/onto/CustomBeanOntologyBuilder.java +++ b/shongo-common-api/src/main/java/jade/content/onto/CustomBeanOntologyBuilder.java @@ -70,6 +70,10 @@ private static boolean isGetter(Method method) c = methodName.charAt(2); } else { + if (methodName.length() < 4) { + // it is too short + return false; + } c = methodName.charAt(3); } if (!Character.isUpperCase(c) && '_' != c) { diff --git a/shongo-common-api/src/main/java/org/joda/time/format/PeriodCzechAffix.java b/shongo-common-api/src/main/java/org/joda/time/format/PeriodCzechAffix.java index db5220594..6f553e12d 100644 --- a/shongo-common-api/src/main/java/org/joda/time/format/PeriodCzechAffix.java +++ b/shongo-common-api/src/main/java/org/joda/time/format/PeriodCzechAffix.java @@ -135,4 +135,16 @@ else if (o1.length() < o2.length()) { } return ~position; } + + @Override + public String[] getAffixes() + { + return new String[] { iSingularText, iFewText, iPluralText }; + } + + @Override + public void finish(Set set) + { + set.add(this); + } } 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..439072995 100644 --- a/shongo-controller-api/pom.xml +++ b/shongo-controller-api/pom.xml @@ -40,6 +40,13 @@ 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/AuxDataFilter.java b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/AuxDataFilter.java new file mode 100644 index 000000000..0315cac05 --- /dev/null +++ b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/AuxDataFilter.java @@ -0,0 +1,37 @@ +package cz.cesnet.shongo.controller.api; + +import cz.cesnet.shongo.api.AbstractComplexType; +import cz.cesnet.shongo.api.DataMap; +import lombok.Data; + +@Data +public class AuxDataFilter extends AbstractComplexType +{ + + private String tagName; + private TagType tagType; + private Boolean enabled; + + private static final String TAG_NAME = "tagName"; + private static final String TAG_TYPE = "tagType"; + private static final String ENABLED = "enabled"; + + @Override + public DataMap toData() + { + DataMap dataMap = super.toData(); + dataMap.set(TAG_NAME, tagName); + dataMap.set(TAG_TYPE, tagType); + dataMap.set(ENABLED, enabled); + return dataMap; + } + + @Override + public void fromData(DataMap dataMap) + { + super.fromData(dataMap); + tagName = dataMap.getString(TAG_NAME); + tagType = dataMap.getEnum(TAG_TYPE, TagType.class); + enabled = dataMap.getBoolean(ENABLED); + } +} 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..5632c0b69 --- /dev/null +++ b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/AuxiliaryData.java @@ -0,0 +1,62 @@ +package cz.cesnet.shongo.controller.api; + +import com.fasterxml.jackson.annotation.JsonIgnore; +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; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class AuxiliaryData extends AbstractComplexType +{ + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private String tagName; + private boolean enabled; + private JsonNode data; + + public AuxiliaryData(String tagName, boolean enabled, JsonNode data) + { + setTagName(tagName); + setEnabled(enabled); + setData(data); + } + + public void setData(JsonNode data) { + this.data = data; + if (data == null) { + this.data = objectMapper.nullNode(); + } + } + + @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.getJsonNode("data"); + } +} diff --git a/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/ReservationDevice.java b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/ReservationDevice.java new file mode 100644 index 000000000..c49316fdc --- /dev/null +++ b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/ReservationDevice.java @@ -0,0 +1,46 @@ +package cz.cesnet.shongo.controller.api; + +import cz.cesnet.shongo.api.DataMap; +import cz.cesnet.shongo.api.IdentifiedComplexType; +import lombok.Getter; +import lombok.Setter; + +/** + * Reservation device authorized to create/list reservations for a particular resource. + */ +@Getter +@Setter +public class ReservationDevice extends IdentifiedComplexType { + private String accessToken; + private String resourceId; + + private static final String ID = "id"; + private static final String ACCESS_TOKEN = "access_token"; + private static final String RESOURCE_ID = "resource_id"; + + + @Override + public DataMap toData() + { + DataMap dataMap = super.toData(); + dataMap.set(ID, id); + dataMap.set(ACCESS_TOKEN, accessToken); + dataMap.set(RESOURCE_ID, resourceId); + return dataMap; + } + + @Override + public void fromData(DataMap dataMap) + { + super.fromData(dataMap); + id = dataMap.getString(ID); + accessToken = dataMap.getString(ACCESS_TOKEN); + resourceId = dataMap.getString(RESOURCE_ID); + } + + @Override + public String toString() + { + return String.format("ReservationDevice (%s, %s)", id, resourceId); + } +} 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..efd92bb21 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 @@ -1,6 +1,8 @@ package cz.cesnet.shongo.controller.api; +import com.fasterxml.jackson.databind.JsonNode; import cz.cesnet.shongo.Technology; +import cz.cesnet.shongo.api.Converter; import cz.cesnet.shongo.api.DataMap; import cz.cesnet.shongo.api.IdentifiedComplexType; import cz.cesnet.shongo.controller.ReservationRequestPurpose; @@ -110,7 +112,7 @@ public class ReservationRequestSummary extends IdentifiedComplexType /** * Resource tags. */ - private String resourceTags; + private List resourceTags = new ArrayList<>(); /** * Specifies whether room has recording service. @@ -127,20 +129,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 JsonNode 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 +510,30 @@ public void setAllowCache(boolean allowCache) this.allowCache = allowCache; } + /** + * @return {@link #auxData} + */ + public JsonNode getAuxData() + { + return auxData; + } + + /** + * @param auxData sets the {@link #auxData} + */ + public void setAuxData(JsonNode auxData) + { + this.auxData = auxData; + } + + /** + * @param auxData sets the {@link #auxData} + */ + public void setAuxData(String auxData) + { + this.auxData = Converter.convertToJsonNode(auxData); + } + private static final String PARENT_RESERVATION_REQUEST_ID = "parentReservationRequestId"; private static final String TYPE = "type"; private static final String DATETIME = "dateTime"; @@ -518,6 +556,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 +584,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 +613,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.getJsonNode(AUX_DATA); } /** diff --git a/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/ResourceSummary.java b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/ResourceSummary.java index a2759c8d6..25ffaec2c 100644 --- a/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/ResourceSummary.java +++ b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/ResourceSummary.java @@ -19,6 +19,11 @@ public class ResourceSummary extends IdentifiedComplexType */ private String userId; + /** + * Type of the resource. + */ + private Type type; + /** * Name of the resource. */ @@ -29,6 +34,11 @@ public class ResourceSummary extends IdentifiedComplexType */ private Set technologies = new HashSet(); + /** + * Tags of the resource. + */ + private Set tags = new HashSet<>(); + /** * Parent resource shongo-id. */ @@ -71,6 +81,11 @@ public class ResourceSummary extends IdentifiedComplexType */ private boolean confirmByOowner; + /** + * Specifies whether resource has capacity. + */ + private boolean hasCapacity; + /** * @return {@link #userId} */ @@ -103,6 +118,20 @@ public void setName(String name) this.name = name; } + /** + * @return {@link #type} + */ + public Type getType() { + return type; + } + + /** + * @param type sets the {@link #type} + */ + public void setType(Type type) { + this.type = type; + } + /** * @return {@link #technologies} */ @@ -130,6 +159,20 @@ public void addTechnology(Technology technology) this.technologies.add(technology); } + /** + * @return {@link #tags} + */ + public Set getTags() { + return tags; + } + + /** + * @param tag to be added to the {@link #tags} + */ + public void addTag(Tag tag) { + tags.add(tag); + } + /** * @return {@link #parentResourceId} */ @@ -214,6 +257,16 @@ public void setConfirmByOowner(boolean confirmByOowner) this.confirmByOowner = confirmByOowner; } + public boolean hasCapacity() + { + return hasCapacity; + } + + public void setHasCapacity(boolean hasCapacity) + { + this.hasCapacity = hasCapacity; + } + public String getRemoteCalendarName() { return remoteCalendarName; } @@ -230,6 +283,12 @@ public void setAllocationOrder(Integer allocationOrder) this.allocationOrder = allocationOrder; } + public enum Type { + ROOM_PROVIDER, + RECORDING_SERVICE, + RESOURCE, + } + private static final String USER_ID = "userId"; private static final String NAME = "name"; private static final String TECHNOLOGIES = "technologies"; @@ -241,7 +300,10 @@ public void setAllocationOrder(Integer allocationOrder) private static final String CALENDAR_URI_KEY = "calendarUriKey"; private static final String DOMAIN_NAME = "domainName"; private static final String CONFIRM_BY_OWNER = "confirmByOwner"; + private static final String HAS_CAPACITY = "hasCapacity"; public static final String REMOTE_CALENDAR_NAME = "remoteCalendarName"; + public static final String TYPE = "type"; + public static final String TAGS = "tags"; @Override public DataMap toData() @@ -258,7 +320,10 @@ public DataMap toData() dataMap.set(CALENDAR_URI_KEY, calendarUriKey); dataMap.set(DOMAIN_NAME, domainName); dataMap.set(CONFIRM_BY_OWNER, confirmByOowner); + dataMap.set(HAS_CAPACITY, hasCapacity); dataMap.set(REMOTE_CALENDAR_NAME, remoteCalendarName); + dataMap.set(TYPE, type); + dataMap.set(TAGS, tags); return dataMap; } @@ -277,6 +342,9 @@ public void fromData(DataMap dataMap) calendarUriKey = dataMap.getString(CALENDAR_URI_KEY); domainName = dataMap.getString(DOMAIN_NAME); confirmByOowner = dataMap.getBool(CONFIRM_BY_OWNER); + hasCapacity = dataMap.getBool(HAS_CAPACITY); remoteCalendarName = dataMap.getString(REMOTE_CALENDAR_NAME); + type = dataMap.getEnum(TYPE, Type.class); + tags = dataMap.getSet(TAGS, Tag.class); } } 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..1953a1e0f 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 @@ -1,15 +1,25 @@ 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.DataMap; import cz.cesnet.shongo.api.IdentifiedComplexType; +import java.util.Objects; + /** * * @author Ondřej Pavelka */ public class Tag extends IdentifiedComplexType { + private static final ObjectMapper objectMapper = new ObjectMapper(); + String name; + TagType type = TagType.DEFAULT; + JsonNode data; public String getName() { return name; @@ -19,13 +29,60 @@ public void setName(String name) { this.name = name; } + public TagType getType() + { + return type; + } + + public void setType(TagType type) + { + this.type = type; + } + + public JsonNode getData() + { + return data; + } + + public void setData(JsonNode data) + { + this.data = data; + } + + public static Tag fromConcat(String concat) + { + String[] parts = concat.split(",", 4); + Tag tag = new Tag(); + tag.setId(parts[0]); + tag.setName(parts[1]); + tag.setType(TagType.valueOf(parts[2])); + if (parts.length > 3) { + try { + tag.setData(objectMapper.readTree(parts[3])); + } catch (JsonProcessingException e) { + throw new RuntimeException("Error parsing tag data", e); + } + } + return tag; + } + + @Override + @JsonIgnore + public String getClassName() { + return super.getClassName(); + } + 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 +91,11 @@ public void fromData(DataMap dataMap) { super.fromData(dataMap); name = dataMap.getString(NAME); + type = dataMap.getEnumRequired(TYPE, TagType.class); + JsonNode jsonNode = dataMap.getJsonNode(DATA); + if (jsonNode != null) { + data = jsonNode; + } } @Override @@ -43,14 +105,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/TagData.java b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/TagData.java new file mode 100644 index 000000000..d6b8c84de --- /dev/null +++ b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/TagData.java @@ -0,0 +1,46 @@ +package cz.cesnet.shongo.controller.api; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.JsonNode; +import cz.cesnet.shongo.api.AbstractComplexType; +import cz.cesnet.shongo.api.DataMap; +import lombok.Data; + +@Data +public class TagData extends AbstractComplexType +{ + + private String name; + private TagType type; + private JsonNode 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; + } + + @Override + public void fromData(DataMap dataMap) + { + super.fromData(dataMap); + name = dataMap.getString(NAME); + type = dataMap.getEnum(TYPE, TagType.class); + data = dataMap.getJsonNode(DATA); + } + + @Override + @JsonIgnore + public String getClassName() + { + return super.getClassName(); + } +} 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-api/src/main/java/cz/cesnet/shongo/controller/api/request/ReservationRequestListRequest.java b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/request/ReservationRequestListRequest.java index a82861dbf..90fccb57e 100644 --- a/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/request/ReservationRequestListRequest.java +++ b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/request/ReservationRequestListRequest.java @@ -6,7 +6,6 @@ import cz.cesnet.shongo.controller.api.AllocationState; import cz.cesnet.shongo.controller.api.ReservationRequestSummary; import cz.cesnet.shongo.controller.api.SecurityToken; -import org.joda.time.DateTime; import org.joda.time.Interval; import java.util.HashSet; @@ -45,9 +44,9 @@ public class ReservationRequestListRequest extends SortableListRequest specificationResourceIds = new HashSet<>(); /** * Restricts the {@link ListResponse} to contain only reservation requests which reuse reservation request with @@ -212,19 +211,19 @@ public void addSpecificationType(ReservationRequestSummary.SpecificationType spe } /** - * @return {@link #specificationResourceId} + * @return {@link #specificationResourceIds} */ - public String getSpecificationResourceId() + public Set getSpecificationResourceIds() { - return specificationResourceId; + return specificationResourceIds; } /** - * @param specificationResourceId sets the {@link #specificationResourceId} + * @param specificationResourceIds sets the {@link #specificationResourceIds} */ - public void setSpecificationResourceId(String specificationResourceId) + public void setSpecificationResourceIds(Set specificationResourceIds) { - this.specificationResourceId = specificationResourceId; + this.specificationResourceIds = specificationResourceIds; } /** @@ -365,7 +364,8 @@ public static enum Sort DATETIME, REUSED_RESERVATION_REQUEST, ROOM_PARTICIPANT_COUNT, - SLOT, + SLOT_START, + SLOT_END, SLOT_NEAREST, STATE, TECHNOLOGY, @@ -377,7 +377,7 @@ public static enum Sort private static final String PARENT_RESERVATION_REQUEST_ID = "parentReservationRequestId"; private static final String SPECIFICATION_TYPES = "specificationTypes"; private static final String SPECIFICATION_TECHNOLOGIES = "specificationTechnologies"; - private static final String SPECIFICATION_RESOURCE_ID = "specificationResourceId"; + private static final String SPECIFICATION_RESOURCE_IDS = "specificationResourceIds"; private static final String REUSED_RESERVATION_REQUEST_ID = "reusedReservationRequestId"; private static final String ALLOCATION_STATE = "allocationState"; private static final String INTERVAL = "interval"; @@ -395,7 +395,7 @@ public DataMap toData() dataMap.set(PARENT_RESERVATION_REQUEST_ID, parentReservationRequestId); dataMap.set(SPECIFICATION_TYPES, specificationTypes); dataMap.set(SPECIFICATION_TECHNOLOGIES, specificationTechnologies); - dataMap.set(SPECIFICATION_RESOURCE_ID, specificationResourceId); + dataMap.set(SPECIFICATION_RESOURCE_IDS, specificationResourceIds); dataMap.set(REUSED_RESERVATION_REQUEST_ID, reusedReservationRequestId); dataMap.set(ALLOCATION_STATE, allocationState); dataMap.set(INTERVAL, interval); @@ -416,7 +416,7 @@ public void fromData(DataMap dataMap) specificationTypes = (Set) dataMap.getSet(SPECIFICATION_TYPES, ReservationRequestSummary.SpecificationType.class); specificationTechnologies = dataMap.getSet(SPECIFICATION_TECHNOLOGIES, Technology.class); - specificationResourceId = dataMap.getString(SPECIFICATION_RESOURCE_ID); + specificationResourceIds = dataMap.getSet(SPECIFICATION_RESOURCE_IDS, String.class); reusedReservationRequestId = dataMap.getString(REUSED_RESERVATION_REQUEST_ID); allocationState = dataMap.getEnum(ALLOCATION_STATE, AllocationState.class); interval = dataMap.getInterval(INTERVAL); diff --git a/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/rpc/AuthorizationService.java b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/rpc/AuthorizationService.java index 6a7c447ce..c014b5143 100644 --- a/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/rpc/AuthorizationService.java +++ b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/rpc/AuthorizationService.java @@ -2,15 +2,14 @@ import cz.cesnet.shongo.api.UserInformation; import cz.cesnet.shongo.api.rpc.Service; -import cz.cesnet.shongo.controller.ObjectRole; import cz.cesnet.shongo.controller.ObjectPermission; +import cz.cesnet.shongo.controller.api.ReservationDevice; import cz.cesnet.shongo.controller.SystemPermission; import cz.cesnet.shongo.controller.api.*; import cz.cesnet.shongo.controller.api.request.*; import java.util.List; import java.util.Map; -import java.util.Set; /** * Interface defining service for accessing Shongo ACL. @@ -194,4 +193,7 @@ public interface AuthorizationService extends Service */ @API public List listReferencedUsers(SecurityToken securityToken); + + @API + public ReservationDevice getReservationDevice(SecurityToken securityToken); } diff --git a/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/rpc/ReservationService.java b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/rpc/ReservationService.java index 41fcc6cbe..1eb21e016 100644 --- a/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/rpc/ReservationService.java +++ b/shongo-controller-api/src/main/java/cz/cesnet/shongo/controller/api/rpc/ReservationService.java @@ -3,6 +3,8 @@ import cz.cesnet.shongo.api.rpc.Service; import cz.cesnet.shongo.controller.api.*; import cz.cesnet.shongo.controller.api.request.*; +import cz.cesnet.shongo.controller.api.AuxDataFilter; +import cz.cesnet.shongo.controller.api.TagData; import java.util.Collection; import java.util.List; @@ -192,4 +194,10 @@ public List getReservationRequestHistory(SecurityToke */ @API public String getCachedResourceReservationsICalendar(ReservationListRequest request); + + @API + /** + * Returns TagData (merged data from tags and aux data) for given reservation request. + */ + public List> getReservationRequestTagData(SecurityToken token, String reservationRequestId, AuxDataFilter filter); } diff --git a/shongo-controller/pom.xml b/shongo-controller/pom.xml index a3266ef6b..a306aa348 100644 --- a/shongo-controller/pom.xml +++ b/shongo-controller/pom.xml @@ -22,6 +22,7 @@ 5.2.2.RELEASE + 2.10.1 @@ -74,11 +75,46 @@ 1.8.4 - + + + + org.springframework.security + spring-security-web + ${spring.version} + - org.codehaus.jackson - jackson-mapper-asl - 1.9.13 + org.springframework.security + spring-security-config + ${spring.version} + + + org.springframework.security + spring-security-oauth2-core + ${spring.version} + + + org.springframework.security + spring-security-oauth2-jose + ${spring.version} + + + org.springframework.security + spring-security-oauth2-resource-server + ${spring.version} + + + + + org.springdoc + springdoc-openapi-ui + 1.4.0 + + + + + com.fasterxml.jackson.datatype + jackson-datatype-joda + ${jackson.version} @@ -201,6 +237,26 @@ ical4j-zoneinfo-outlook 1.0.3 + + + + org.projectlombok + lombok + provided + + + + org.mockito + mockito-core + 5.11.0 + test + + + + net.bytebuddy + byte-buddy + 1.14.12 + diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/Controller.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/Controller.java index 7a27f8b8b..2bf4dc3e2 100644 --- a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/Controller.java +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/Controller.java @@ -1,6 +1,5 @@ package cz.cesnet.shongo.controller; -import com.google.common.base.Strings; import cz.cesnet.shongo.api.rpc.Service; import cz.cesnet.shongo.controller.api.UserSettings; import cz.cesnet.shongo.controller.api.jade.ServiceImpl; @@ -11,20 +10,18 @@ import cz.cesnet.shongo.controller.calendar.CalendarManager; import cz.cesnet.shongo.controller.calendar.connector.CalDAVConnector; import cz.cesnet.shongo.controller.calendar.connector.CalendarConnector; -import cz.cesnet.shongo.controller.domains.BasicAuthFilter; import cz.cesnet.shongo.controller.domains.InterDomainAgent; -import cz.cesnet.shongo.controller.domains.SSLClientCertFilter; import cz.cesnet.shongo.controller.executor.Executor; import cz.cesnet.shongo.controller.notification.executor.EmailNotificationExecutor; import cz.cesnet.shongo.controller.notification.executor.NotificationExecutor; import cz.cesnet.shongo.controller.notification.NotificationManager; +import cz.cesnet.shongo.controller.rest.RESTApiServer; import cz.cesnet.shongo.controller.scheduler.Preprocessor; import cz.cesnet.shongo.controller.scheduler.Scheduler; import cz.cesnet.shongo.controller.util.NativeQuery; import cz.cesnet.shongo.jade.Agent; import cz.cesnet.shongo.jade.Container; import cz.cesnet.shongo.ssl.ConfiguredSSLContext; -import cz.cesnet.shongo.ssl.SSLCommunication; import cz.cesnet.shongo.util.Logging; import cz.cesnet.shongo.util.Timer; import org.apache.commons.cli.*; @@ -35,21 +32,15 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletMapping; import org.eclipse.jetty.util.ssl.SslContextFactory; -import org.eclipse.jetty.webapp.WebAppContext; import org.joda.time.DateTimeZone; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.web.servlet.DispatcherServlet; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; -import javax.servlet.DispatcherType; import java.io.IOException; import java.io.InputStream; -import java.net.URL; -import java.security.InvalidAlgorithmParameterException; -import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; @@ -537,7 +528,7 @@ public void startAll() throws Exception { start(); startRpc(); startJade(); - startInterDomainRESTApi(); + startRESTApi(); startWorkerThread(); startComponents(); } @@ -629,87 +620,21 @@ public Container startJade() return jadeContainer; } - public Server startInterDomainRESTApi() throws NoSuchAlgorithmException, CertificateException, InvalidAlgorithmParameterException, IOException, KeyStoreException { - if (configuration.isInterDomainConfigured()) { - logger.info("Starting Inter Domain REST server on {}:{}...", - configuration.getInterDomainHost(), configuration.getInterDomainPort()); - - restServer = new Server(); - // Configure SSL - ConfiguredSSLContext.getInstance().loadConfiguration(configuration); - - // Create web app - WebAppContext webAppContext = new WebAppContext(); - String servletPath = "/*"; - webAppContext.addServlet(new ServletHolder("interDomain", DispatcherServlet.class), servletPath); - webAppContext.setParentLoaderPriority(true); - - URL resourceBaseUrl = Controller.class.getClassLoader().getResource("WEB-INF"); - if (resourceBaseUrl == null) { - throw new RuntimeException("WEB-INF is not in classpath."); - } - String resourceBase = resourceBaseUrl.toExternalForm().replace("/WEB-INF", "/"); - webAppContext.setResourceBase(resourceBase); - - final HttpConfiguration http_config = new HttpConfiguration(); - - // Configure HTTPS connector - http_config.setSecureScheme(HttpScheme.HTTPS.asString()); - http_config.setSecurePort(configuration.getInterDomainPort()); - final HttpConfiguration https_config = new HttpConfiguration(http_config); - https_config.addCustomizer(new SecureRequestCustomizer()); - - - final SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); - KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); - trustStore.load(null); - // Load certificates of foreign domain's CAs - for (String certificatePath : configuration.getForeignDomainsCaCertFiles()) { - trustStore.setCertificateEntry(certificatePath.substring(0, certificatePath.lastIndexOf('.')), - SSLCommunication.readPEMCert(certificatePath)); - } - sslContextFactory.setKeyStorePath(configuration.getInterDomainSslKeyStore()); - sslContextFactory.setKeyStoreType(configuration.getInterDomainSslKeyStoreType()); - sslContextFactory.setKeyStorePassword(configuration.getInterDomainSslKeyStorePassword()); - sslContextFactory.setTrustStore(trustStore); - if (configuration.requiresClientPKIAuth()) { - // Enable forced client auth - sslContextFactory.setNeedClientAuth(true); - // Enable SSL client filter by certificates - EnumSet filterTypes = EnumSet.of(DispatcherType.REQUEST); - webAppContext.addFilter(SSLClientCertFilter.class, servletPath, filterTypes); - } - else { - EnumSet filterTypes = EnumSet.of(DispatcherType.REQUEST); - webAppContext.addFilter(BasicAuthFilter.class, servletPath, filterTypes); - } - - final ServerConnector httpsConnector = new ServerConnector(restServer, - new SslConnectionFactory(sslContextFactory, "http/1.1"), - new HttpConnectionFactory(https_config)); - String host = configuration.getInterDomainHost(); - if (!Strings.isNullOrEmpty(host)) { - httpsConnector.setHost(host); - } - httpsConnector.setPort(configuration.getInterDomainPort()); - httpsConnector.setIdleTimeout(configuration.getInterDomainCommandTimeout()); - - + /** + * Creates a Jetty REST api server for frontend and inter-domain extension. + */ + public Server startRESTApi() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { + logger.info("Starting REST api server on {}:{}...", + configuration.getRESTApiHost(), configuration.getRESTApiPort()); - restServer.setConnectors(new Connector[]{httpsConnector}); + restServer = RESTApiServer.start(configuration); - restServer.setHandler(webAppContext); - try { - restServer.start(); - logger.info("Inter Domain REST server successfully started."); - } catch (Exception exception) { - throw new RuntimeException(exception); - } - - this.interDomainInitialized = true; - return restServer; + if (configuration.isInterDomainConfigured()) { + interDomainInitialized = true; } - return null; + logger.info("REST api server successfully started."); + + return restServer; } /** @@ -803,7 +728,7 @@ public void stop() } if (restServer != null) { - logger.info("Stopping Controller Inter Domain REST server..."); + logger.info("Stopping Controller REST server..."); try { restServer.stop(); } catch (Exception exception) { diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/ControllerConfiguration.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/ControllerConfiguration.java index d9e1cffbf..24984dcf5 100644 --- a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/ControllerConfiguration.java +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/ControllerConfiguration.java @@ -2,21 +2,29 @@ import com.google.common.base.Strings; import cz.cesnet.shongo.PersonInformation; +import cz.cesnet.shongo.controller.authorization.ReservationDeviceConfig; import cz.cesnet.shongo.controller.booking.executable.Executable; import cz.cesnet.shongo.controller.settings.UserSessionSettings; import cz.cesnet.shongo.ssl.SSLCommunication; import cz.cesnet.shongo.util.PatternParser; import org.apache.commons.configuration.CombinedConfiguration; +import org.apache.commons.configuration.HierarchicalConfiguration; import org.apache.commons.configuration.tree.NodeCombiner; import org.apache.commons.configuration.tree.UnionCombiner; import org.joda.time.Duration; import org.joda.time.Period; import org.postgresql.util.Base64; +import java.net.MalformedURLException; +import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.NoSuchElementException; import java.util.regex.MatchResult; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * Configuration for the {@link Controller}. @@ -59,16 +67,22 @@ public class ControllerConfiguration extends CombinedConfiguration public static final String JADE_AGENT_NAME = "jade.agent-name"; public static final String JADE_PLATFORM_ID = "jade.platform-id"; + /** + * REST api configuration + */ + public static final String REST_API = "rest-api"; + public static final String REST_API_HOST = "rest-api.host"; + public static final String REST_API_PORT = "rest-api.port"; + public static final String REST_API_ORIGIN = "rest-api.origin"; + public static final String REST_API_SSL_KEY_STORE = REST_API + ".ssl-key-store"; + public static final String REST_API_SSL_KEY_STORE_TYPE = REST_API + ".ssl-key-store-type"; + public static final String REST_API_SSL_KEY_STORE_PASSWORD = REST_API + ".ssl-key-store-password"; + /** * Interdomains configuration */ public static final String INTERDOMAIN = "domain.inter-domain-connection"; - public static final String INTERDOMAIN_HOST = INTERDOMAIN + ".host"; - public static final String INTERDOMAIN_PORT = INTERDOMAIN + ".port"; public static final String INTERDOMAIN_PKI_CLIENT_AUTH = INTERDOMAIN + ".pki-client-auth"; - public static final String INTERDOMAIN_SSL_KEY_STORE = INTERDOMAIN + ".ssl-key-store"; - public static final String INTERDOMAIN_SSL_KEY_STORE_TYPE = INTERDOMAIN + ".ssl-key-store-type"; - public static final String INTERDOMAIN_SSL_KEY_STORE_PASSWORD = INTERDOMAIN + ".ssl-key-store-password"; public static final String INTERDOMAIN_TRUSTED_CA_CERT_FILES = INTERDOMAIN + ".ssl-trust-store.ca-certificate"; public static final String INTERDOMAIN_COMMAND_TIMEOUT = INTERDOMAIN + ".command-timeout"; public static final String INTERDOMAIN_CACHE_REFRESH_RATE = INTERDOMAIN + ".cache-refresh-rate"; @@ -101,7 +115,13 @@ public class ControllerConfiguration extends CombinedConfiguration public static final String CALDAV_BASIC_AUTH_USERNAME = "caldav-connector.basic-auth.username"; public static final String CALDAV_BASIC_AUTH_PASSWORD = "caldav-connector.basic-auth.password"; - + /** + * Tags configuration. + */ + public static final String VEHICLE_TAG = "tags.vehicle"; + public static final String PARKING_PLACE_TAG = "tags.parking-place"; + public static final String MEETING_ROOM_TAG = "tags.meeting-room"; + public static final String DEVICE_TAG = "tags.device"; /** * Period in which the executor works. @@ -205,6 +225,11 @@ public class ControllerConfiguration extends CombinedConfiguration */ public static final String SECURITY_AUTHORIZATION_RESERVATION = "security.authorization.reservation"; + /** + * Configures devices which get access to the system and reservation privilege for a particular resource. + */ + public static final String SECURITY_AUTHORIZATION_RESERVATION_DEVICE = "security.authorization.reservation-devices.device"; + /** * Url where user can change his settings. */ @@ -261,7 +286,6 @@ public Period getPeriod(String key) } /** - * @return timeout to receive response when performing commands from agent */ public Duration getJadeCommandTimeout() @@ -294,6 +318,27 @@ public int getRpcPort() return getInt(RPC_PORT); } + /** + * @return XML-RPC url + */ + public URL getRpcUrl() throws MalformedURLException + { + int rpcPort; + + try { + rpcPort = getRpcPort(); + } + catch (NoSuchElementException e) { + rpcPort = 8181; + } + String scheme = (getRpcSslKeyStore() != null) ? "https" : "http"; + String rpcHost = getRpcHost(true); + String urlString = (rpcHost != null) + ? String.format("%s://%s:%d", scheme, rpcHost, rpcPort) + : String.format("%s://%s:%d", scheme, getRESTApiHost(), rpcPort); + return new URL(urlString); + } + /** * @return XML-RPC ssl key store */ @@ -481,63 +526,72 @@ else if (name.equals("domain.shortName")) { public boolean isInterDomainConfigured() { - if (getInterDomainPort() != null) { - if (requiresClientPKIAuth() && hasInterDomainPKI()) { - return true; - } - if (hasInterDomainBasicAuth()) { - return true; - } - } - return false; - } - - public boolean hasInterDomainPKI() - { - if (Strings.isNullOrEmpty(getInterDomainSslKeyStore()) - || Strings.isNullOrEmpty(getInterDomainSslKeyStoreType()) - || Strings.isNullOrEmpty(getInterDomainSslKeyStorePassword())) { - return false; + if (requiresClientPKIAuth() && hasRESTApiPKI()) { + return true; } - return true; + return hasInterDomainBasicAuth(); } public boolean hasInterDomainBasicAuth() { - if (Strings.isNullOrEmpty(getInterDomainBasicAuthPasswordHash())) { - return false; - } - return true; + return !Strings.isNullOrEmpty(getInterDomainBasicAuthPasswordHash()); } - public String getInterDomainHost() + public String getRESTApiHost() { - return getString(ControllerConfiguration.INTERDOMAIN_HOST); + String host = getString(ControllerConfiguration.REST_API_HOST); + return host != null ? host : "localhost"; } - public Integer getInterDomainPort() + public Integer getRESTApiPort() { - return getInteger(ControllerConfiguration.INTERDOMAIN_PORT, null); + Integer port = getInteger(ControllerConfiguration.REST_API_PORT, null); + return port != null ? port : 9999; } /** - * Returns true if PKI auth is selected to be used - * @return + * @return list of allowed origins for CORS configuration. */ - public boolean requiresClientPKIAuth() + public synchronized List getRESTApiAllowedOrigins() { - return getBoolean(ControllerConfiguration.INTERDOMAIN_PKI_CLIENT_AUTH, false); + return getList(REST_API_ORIGIN).stream().map(origin -> (String) origin).collect(Collectors.toList()); } - public String getInterDomainSslKeyStore() + public String getRESTApiSslKeyStore() { - String sslKeyStore = getString(ControllerConfiguration.INTERDOMAIN_SSL_KEY_STORE); + String sslKeyStore = getString(ControllerConfiguration.REST_API_SSL_KEY_STORE); if (sslKeyStore == null || sslKeyStore.trim().isEmpty()) { return null; } return sslKeyStore; } + public String getRESTApiSslKeyStoreType() + { + return getString(ControllerConfiguration.REST_API_SSL_KEY_STORE_TYPE); + } + + public String getRESTApiSslKeyStorePassword() + { + return getString(ControllerConfiguration.REST_API_SSL_KEY_STORE_PASSWORD); + } + + public boolean hasRESTApiPKI() + { + return !Strings.isNullOrEmpty(getRESTApiSslKeyStore()) + && !Strings.isNullOrEmpty(getRESTApiSslKeyStoreType()) + && !Strings.isNullOrEmpty(getRESTApiSslKeyStorePassword()); + } + + /** + * Returns true if PKI auth is selected to be used + * @return + */ + public boolean requiresClientPKIAuth() + { + return getBoolean(ControllerConfiguration.INTERDOMAIN_PKI_CLIENT_AUTH, false); + } + public String getInterDomainBasicAuthPasswordHash() { String password = getString(ControllerConfiguration.INTERDOMAIN_BASIC_AUTH_PASSWORD); @@ -547,14 +601,6 @@ public String getInterDomainBasicAuthPasswordHash() return SSLCommunication.hashPassword(password.getBytes()); } - public String getInterDomainSslKeyStoreType() { - return getString(ControllerConfiguration.INTERDOMAIN_SSL_KEY_STORE_TYPE); - } - - public String getInterDomainSslKeyStorePassword() { - return getString(ControllerConfiguration.INTERDOMAIN_SSL_KEY_STORE_PASSWORD); - } - public List getForeignDomainsCaCertFiles() { List caCertFiles = new ArrayList(); for (Object o : getList(ControllerConfiguration.INTERDOMAIN_TRUSTED_CA_CERT_FILES)) { @@ -590,4 +636,54 @@ public String getCalDAVEncodedBasicAuth() String authStringEnc = Base64.encodeBytes(authString.getBytes(StandardCharsets.UTF_8)); return authStringEnc; } + + + /** + * @return name of tag for meeting rooms + */ + public String getMeetingRoomTagName() + { + return getString(MEETING_ROOM_TAG); + } + + /** + * @return name of tag for cars + */ + public String getVehicleTagName() + { + return getString(VEHICLE_TAG); + } + + /** + * @return name of tag for parking places + */ + public String getParkingPlaceTagName() + { + return getString(PARKING_PLACE_TAG); + } + + /** + * @return name of tag for devices + */ + public String getDeviceTagName() + { + return getString(DEVICE_TAG); + } + + /** + * @return list of reservation devices. + */ + public List getReservationDevices() { + List deviceConfigs = new ArrayList<>(); + + for (HierarchicalConfiguration conf : configurationsAt(SECURITY_AUTHORIZATION_RESERVATION_DEVICE)) { + String accessToken = conf.getString("access-token"); + String resourceId = conf.getString("resource-id"); + String deviceId = conf.getString("device-id"); + ReservationDeviceConfig deviceConfig = new ReservationDeviceConfig(deviceId, accessToken, resourceId); + deviceConfigs.add(deviceConfig); + } + + return deviceConfigs; + } } diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/EmailSender.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/EmailSender.java index 28ed3c661..8ee1ecaa4 100644 --- a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/EmailSender.java +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/EmailSender.java @@ -271,6 +271,14 @@ public Email(String recipient, Collection replyTo, String subject, Strin setContent(content); } + public Email(Collection recipient, String replyTo, String subject, String content) + { + addRecipients(recipient); + addReplyTo(replyTo); + setSubject(subject); + setContent(content); + } + public void addRecipient(String recipient) { try { diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/api/rpc/AuthorizationServiceImpl.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/api/rpc/AuthorizationServiceImpl.java index 5ec432d3d..35311fbab 100644 --- a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/api/rpc/AuthorizationServiceImpl.java +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/api/rpc/AuthorizationServiceImpl.java @@ -11,9 +11,9 @@ import cz.cesnet.shongo.controller.api.request.*; import cz.cesnet.shongo.controller.authorization.Authorization; import cz.cesnet.shongo.controller.authorization.AuthorizationManager; +import cz.cesnet.shongo.controller.authorization.ReservationDeviceConfig; import cz.cesnet.shongo.controller.authorization.UserIdSet; import cz.cesnet.shongo.controller.booking.ObjectIdentifier; -import cz.cesnet.shongo.controller.booking.person.UserPerson; import cz.cesnet.shongo.controller.booking.request.AbstractReservationRequest; import cz.cesnet.shongo.controller.booking.Allocation; import cz.cesnet.shongo.controller.booking.request.ReservationRequest; @@ -859,6 +859,22 @@ public List listReferencedUsers(SecurityToken securityToken) } } + @Override + public ReservationDevice getReservationDevice(SecurityToken securityToken) { + Optional deviceConfigOpt = authorization.getReservationDeviceByToken(securityToken.getAccessToken()); + + if (deviceConfigOpt.isEmpty()) { + return null; + } + + ReservationDeviceConfig deviceConfig = deviceConfigOpt.get(); + ReservationDevice device = new ReservationDevice(); + device.setId(deviceConfig.getDeviceId()); + device.setAccessToken(deviceConfig.getAccessToken()); + device.setResourceId(deviceConfig.getResourceId()); + return device; + } + /** * @param objectId of object which should be checked for existence * @param entityManager which can be used 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..4fdbe0ab1 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,9 +8,12 @@ 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.api.TagData; import cz.cesnet.shongo.controller.authorization.Authorization; import cz.cesnet.shongo.controller.authorization.AuthorizationManager; +import cz.cesnet.shongo.controller.authorization.ReservationDeviceConfig; import cz.cesnet.shongo.controller.booking.Allocation; import cz.cesnet.shongo.controller.booking.ObjectIdentifier; import cz.cesnet.shongo.controller.booking.datetime.PeriodicDateTime; @@ -18,6 +21,7 @@ import cz.cesnet.shongo.controller.booking.request.*; import cz.cesnet.shongo.controller.booking.request.AbstractReservationRequest; import cz.cesnet.shongo.controller.booking.request.ReservationRequest; +import cz.cesnet.shongo.controller.api.AuxDataFilter; import cz.cesnet.shongo.controller.booking.reservation.*; import cz.cesnet.shongo.controller.booking.resource.*; import cz.cesnet.shongo.controller.booking.resource.Resource; @@ -38,6 +42,7 @@ import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import java.util.*; +import java.util.stream.Collectors; /** * Implementation of {@link ReservationService}. @@ -659,7 +664,7 @@ public Boolean confirmReservationRequest(SecurityToken securityToken, String res requestListRequest.setInterval(reservationRequest.getSlot()); requestListRequest.setIntervalDateOnly(false); requestListRequest.setAllocationState(AllocationState.CONFIRM_AWAITING); - requestListRequest.setSpecificationResourceId(resourceId); + requestListRequest.setSpecificationResourceIds(new HashSet<>(Collections.singleton(resourceId))); ListResponse listResponse = listOwnedResourcesReservationRequests(requestListRequest); try { @@ -1030,10 +1035,20 @@ public ListResponse listReservationRequests(Reservati EntityManager entityManager = entityManagerFactory.createEntityManager(); try { QueryFilter queryFilter = new QueryFilter("reservation_request_summary", true); + + Optional deviceUser = authorization.getReservationDeviceByToken(securityToken.getAccessToken()); - // List only reservation requests which is current user permitted to read - queryFilter.addFilterId("allocation_id", authorization, securityToken, - Allocation.class, ObjectPermission.READ); + if (deviceUser.isEmpty()) { + // List only reservation requests which is current user permitted to read + queryFilter.addFilterId("allocation_id", authorization, securityToken, + Allocation.class, ObjectPermission.READ); + } else { + // List only reservation requests of resource which reservation device has access to + String resourceId = deviceUser.get().getResourceId(); + long resourceIdNum = ObjectIdentifier.parse(resourceId).getPersistenceId(); + queryFilter.addFilter("specification_summary.resource_id = :deviceResourceId"); + queryFilter.addFilterParameter("deviceResourceId", resourceIdNum); + } // List only reservation requests which are requested (but latest versions of them) if (request.getReservationRequestIds().size() > 0) { @@ -1102,11 +1117,13 @@ public ListResponse listReservationRequests(Reservati } // Filter specification resource id - String specificationResourceId = request.getSpecificationResourceId(); - if (specificationResourceId != null) { - queryFilter.addFilter("specification_summary.resource_id = :resource_id"); - queryFilter.addFilterParameter("resource_id", - ObjectIdentifier.parseLocalId(specificationResourceId, ObjectType.RESOURCE)); + Set specificationResourceIds = request.getSpecificationResourceIds(); + if (!specificationResourceIds.isEmpty()) { + Set resourceIds = specificationResourceIds.stream() + .map(id -> ObjectIdentifier.parseLocalId(id, ObjectType.RESOURCE)) + .collect(Collectors.toSet()); + queryFilter.addFilter("specification_summary.resource_id IN :resource_ids"); + queryFilter.addFilterParameter("resource_ids", resourceIds); } String reusedReservationRequestId = request.getReusedReservationRequestId(); @@ -1200,7 +1217,10 @@ else if (reusedReservationRequestId.equals(ReservationRequestListRequest.FILTER_ case ROOM_PARTICIPANT_COUNT: queryOrderBy = "specification_summary.room_participant_count"; break; - case SLOT: + case SLOT_START: + queryOrderBy = "reservation_request_summary.slot_start"; + break; + case SLOT_END: queryOrderBy = "reservation_request_summary.slot_end"; break; case SLOT_NEAREST: @@ -1274,11 +1294,13 @@ public ListResponse listOwnedResourcesReservationRequ } // Filter specification resource id - String specificationResourceId = request.getSpecificationResourceId(); - if (specificationResourceId != null) { - queryFilter.addFilter("specification_summary.resource_id = :resource_id"); - queryFilter.addFilterParameter("resource_id", - ObjectIdentifier.parseLocalId(specificationResourceId, ObjectType.RESOURCE)); + Set specificationResourceIds = request.getSpecificationResourceIds(); + if (!specificationResourceIds.isEmpty()) { + Set resourceIds = specificationResourceIds.stream() + .map(id -> ObjectIdentifier.parseLocalId(id, ObjectType.RESOURCE)) + .collect(Collectors.toSet()); + queryFilter.addFilter("specification_summary.resource_id IN :resource_ids"); + queryFilter.addFilterParameter("resource_ids", resourceIds); } // List only latest versions of a reservation requests (no it's modifications or deleted requests) @@ -1323,7 +1345,10 @@ public ListResponse listOwnedResourcesReservationRequ case ROOM_PARTICIPANT_COUNT: queryOrderBy = "specification_summary.room_participant_count"; break; - case SLOT: + case SLOT_START: + queryOrderBy = "reservation_request_summary.slot_start"; + break; + case SLOT_END: queryOrderBy = "reservation_request_summary.slot_end"; break; case SLOT_NEAREST: @@ -1828,6 +1853,33 @@ public String getCachedResourceReservationsICalendar (ReservationListRequest req } + @Override + public List> getReservationRequestTagData(SecurityToken securityToken, String reservationRequestId, AuxDataFilter filter) { + authorization.validate(securityToken); + checkNotNull("reservationRequestId", reservationRequestId); + + EntityManager entityManager = entityManagerFactory.createEntityManager(); + ReservationRequestManager reservationRequestManager = new ReservationRequestManager(entityManager); + ObjectIdentifier objectId = ObjectIdentifier.parse(reservationRequestId, ObjectType.RESERVATION_REQUEST); + + try { + cz.cesnet.shongo.controller.booking.request.AbstractReservationRequest reservationRequest = + reservationRequestManager.get(objectId.getPersistenceId()); + + if (!authorization.hasObjectPermission(securityToken, reservationRequest, ObjectPermission.READ)) { + ControllerReportSetHelper.throwSecurityNotAuthorizedFault("read reservation request %s", objectId); + } + + return reservationRequestManager.getTagData(objectId.getPersistenceId(), filter) + .stream() + .map(tagData -> tagData.toApi()) + .collect(Collectors.toList()); + } + finally { + entityManager.close(); + } + } + /** * Check whether resource with given resourceId is cached and it has public calendar. * @@ -2041,7 +2093,14 @@ 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(Tag::fromConcat) + .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/api/rpc/ResourceServiceImpl.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/api/rpc/ResourceServiceImpl.java index ecc479871..a1f744a9d 100644 --- a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/api/rpc/ResourceServiceImpl.java +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/api/rpc/ResourceServiceImpl.java @@ -449,6 +449,16 @@ else if (capabilityClass.equals(RecordingCapability.class)) { resourceSummary.setCalendarUriKey(record[9].toString()); } resourceSummary.setConfirmByOowner((Boolean) record[10]); + String type = record[11].toString(); + resourceSummary.setType(ResourceSummary.Type.valueOf(type.trim())); + if (record[12] != null) { + String resourceTags = (String) record[12]; + Arrays.stream(resourceTags.split("\\|")) + .map(String::trim) + .map(Tag::fromConcat) + .forEach(resourceSummary::addTag); + } + resourceSummary.setHasCapacity((Boolean) record[13]); response.addItem(resourceSummary); } } diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/authorization/Authorization.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/authorization/Authorization.java index 1a4a17b0e..5971d16f5 100644 --- a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/authorization/Authorization.java +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/authorization/Authorization.java @@ -1,5 +1,6 @@ package cz.cesnet.shongo.controller.authorization; +import com.google.common.collect.ImmutableList; import cz.cesnet.shongo.PersistentObject; import cz.cesnet.shongo.TodoImplementException; import cz.cesnet.shongo.api.UserInformation; @@ -8,18 +9,17 @@ import cz.cesnet.shongo.controller.acl.AclEntry; import cz.cesnet.shongo.controller.api.*; import cz.cesnet.shongo.controller.booking.Allocation; +import cz.cesnet.shongo.controller.booking.ObjectIdentifier; import cz.cesnet.shongo.controller.booking.ObjectTypeResolver; import cz.cesnet.shongo.controller.booking.person.UserPerson; import cz.cesnet.shongo.controller.booking.request.AbstractReservationRequest; import cz.cesnet.shongo.controller.domains.InterDomainAgent; import cz.cesnet.shongo.controller.settings.UserSessionSettings; -import org.apache.http.MethodNotSupportedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; -import javax.persistence.Query; import java.util.*; /** @@ -108,6 +108,11 @@ public abstract class Authorization */ private AuthorizationExpression reservationExpression; + /** + * List of devices authorized to make reservations on a particular resource. + */ + private final ImmutableList reservationDevices; + /** * Constructor. * @@ -155,6 +160,8 @@ protected Long getObjectId(PersistentObject object) configuration.getString(ControllerConfiguration.SECURITY_AUTHORIZATION_OPERATOR), this); this.reservationExpression = new AuthorizationExpression( configuration.getString(ControllerConfiguration.SECURITY_AUTHORIZATION_RESERVATION), this); + + this.reservationDevices = ImmutableList.copyOf(configuration.getReservationDevices()); } /** @@ -168,6 +175,7 @@ protected void initialize() this.administratorExpression.evaluate(rootUserInformation, rootUserAuthorizationData); this.operatorExpression.evaluate(rootUserInformation, rootUserAuthorizationData); this.reservationExpression.evaluate(rootUserInformation, rootUserAuthorizationData); + this.createReservationDeviceAclEntries(); } /** @@ -194,6 +202,14 @@ public void clearCache() cache.clear(); } + public Optional getReservationDeviceById(String id) { + return listReservationDevices().stream().filter(device -> device.getDeviceId().equals(id)).findFirst(); + } + + public Optional getReservationDeviceByToken(String accessToken) { + return listReservationDevices().stream().filter(device -> device.getAccessToken().equals(accessToken)).findFirst(); + } + /** * Validate given {@code securityToken}. * @@ -283,6 +299,12 @@ public final UserInformation getUserInformation(SecurityToken securityToken) public final UserInformation getUserInformation(String userId) throws ControllerReportSet.UserNotExistsException { + Optional reservationDevice = getReservationDeviceById(userId); + + if (reservationDevice.isPresent()) { + return reservationDevice.get().getUserData().getUserInformation(); + } + if (UserInformation.isLocal(userId)) { UserData userData = getUserData(userId); return userData.getUserInformation(); @@ -335,6 +357,14 @@ public final UserData getUserData(String userId) if (userId.equals(ROOT_USER_ID)) { return ROOT_USER_DATA; } + + // Reservation device user + Optional reservationDevice = getReservationDeviceById(userId); + + if (reservationDevice.isPresent()) { + return reservationDevice.get().getUserData(); + } + UserData userData; if (cache.hasUserDataByUserId(userId)) { userData = cache.getUserDataByUserId(userId); @@ -402,13 +432,16 @@ public final Collection listUserInformation(Set filterU { logger.debug("Retrieving list of user information..."); List userInformationList = new LinkedList(); - // Remove root id from request, which is static if contains. - if (filterUserIds != null && filterUserIds.contains(ROOT_USER_ID)) { - filterUserIds = new HashSet<>(filterUserIds); - filterUserIds.remove(ROOT_USER_ID); - // Add root user information to result - userInformationList.add(ROOT_USER_DATA.getUserInformation()); + + // Remove root ID from request and add user data if filter contains root ID. + checkForStaticUser(filterUserIds, userInformationList, ROOT_USER_DATA); + + // Remove reservation device ids from request and add their user data if filter contains their ID. + for (ReservationDeviceConfig reservationDevice : listReservationDevices()) { + UserData deviceUser = reservationDevice.getUserData(); + checkForStaticUser(filterUserIds, userInformationList, deviceUser); } + for (UserData userData : onListUserData(filterUserIds, search)) { userInformationList.add(userData.getUserInformation()); } @@ -702,6 +735,7 @@ public UserSessionSettings getUserSessionSettings(SecurityToken securityToken) /** * @param userSessionSettings to be updated */ + // TODO toto sa zavola public void updateUserSessionSettings(UserSessionSettings userSessionSettings) { SecurityToken securityToken = userSessionSettings.getSecurityToken(); @@ -1031,6 +1065,10 @@ public UserAuthorizationData getUserAuthorizationData(SecurityToken securityToke return userAuthorizationData; } + public Collection listReservationDevices() { + return reservationDevices; + } + /** * Fetch {@link AclUserState} for given {@code userId}. * @@ -1044,8 +1082,10 @@ private AclUserState fetchAclUserState(String userId) if (userId != null) { aclIdentities.add(aclProvider.getIdentity(AclIdentityType.USER, userId)); } - for (String groupId : listUserGroupIds(userId)) { - aclIdentities.add(aclProvider.getIdentity(AclIdentityType.GROUP, groupId)); + if (getReservationDeviceById(userId).isEmpty()) { + for (String groupId : listUserGroupIds(userId)) { + aclIdentities.add(aclProvider.getIdentity(AclIdentityType.GROUP, groupId)); + } } aclIdentities.add(aclProvider.getIdentity(AclIdentityType.GROUP, EVERYONE_GROUP_ID)); EntityManager entityManager = entityManagerFactory.createEntityManager(); @@ -1085,6 +1125,25 @@ private AclObjectState fetchAclObjectState(AclObjectIdentity aclObjectIdentity) return aclObjectState; } + /** + * Checks if filter contains user ID and if yes then adds the user data to the list and removes the ID from the filter. + * + * @param filterUserIds User ID filter. + * @param userInformationList List of user information. + * @param userData User data to add if filter contains their ID. + */ + private void checkForStaticUser(Set filterUserIds, List userInformationList, UserData userData) { + String userId = userData.getUserId(); + + if (filterUserIds != null && filterUserIds.contains(userId)) { + filterUserIds = new HashSet<>(filterUserIds); + filterUserIds.remove(userId); + + // Add user information to result + userInformationList.add(userData.getUserInformation()); + } + } + /** * Add given {@code aclEntry} to the {@link AuthorizationCache}. * @@ -1210,4 +1269,36 @@ public static Authorization getInstance() throws IllegalStateException } return authorization; } + + private void createReservationDeviceAclEntries() { + EntityManager entityManager = entityManagerFactory.createEntityManager(); + AuthorizationManager authManager = new AuthorizationManager(entityManager, this); + + listReservationDevices().forEach(device -> { + authManager.beginTransaction(); + entityManager.getTransaction().begin(); + + try { + String userId = device.getUserData().getUserId(); + String resourceId = device.getResourceId(); + ObjectIdentifier objectIdentifier = ObjectIdentifier.parse(resourceId); + logger.info("Creating ACL entry for reservation device {} for resource {}", device.getUserData().getUserId(), objectIdentifier); + + SecurityToken securityToken = new SecurityToken(device.getAccessToken()); + securityToken.setUserInformation(device.getUserData().getUserInformation()); + + PersistentObject object = entityManager.find(objectIdentifier.getObjectClass(), + objectIdentifier.getPersistenceId()); + authManager.createAclEntry(AclIdentityType.USER, userId, object, ObjectRole.RESERVATION); + + entityManager.getTransaction().commit(); + authManager.commitTransaction(securityToken); + } catch (Error err) { + entityManager.getTransaction().rollback(); + authManager.rollbackTransaction(); + } + }); + + entityManager.close(); + } } diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/authorization/ReservationDeviceConfig.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/authorization/ReservationDeviceConfig.java new file mode 100644 index 000000000..a07e423ed --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/authorization/ReservationDeviceConfig.java @@ -0,0 +1,49 @@ +package cz.cesnet.shongo.controller.authorization; + +import cz.cesnet.shongo.api.UserInformation; +import lombok.Getter; +import javax.validation.constraints.NotNull; + +/** + * Configuration for a physical device which can make reservations for a particular resource. + * + * @author Michal Drobňák + */ +@Getter +public final class ReservationDeviceConfig { + private final String accessToken; + private final String resourceId; + private final String deviceId; + private final UserData userData; + + public ReservationDeviceConfig(@NotNull String deviceId, @NotNull String accessToken, @NotNull String resourceId) { + this.accessToken = accessToken; + this.resourceId = resourceId; + this.deviceId = deviceId; + this.userData = createUserData(); + } + + @Override + public String toString() { + return "ReservationDeviceConfig{" + + "accessToken='" + accessToken + '\'' + + ", resourceId='" + resourceId + '\'' + + '}'; + } + + private UserData createUserData() { + UserData userData = new UserData(); + UserInformation userInformation = userData.getUserInformation(); + UserAuthorizationData userAuthData = new UserAuthorizationData(UserAuthorizationData.LOA_EXTENDED); + + String name = "Reservation Device For " + resourceId; + + userInformation.setUserId(deviceId); + userInformation.setFirstName("Reservation"); + userInformation.setLastName("Device"); + userInformation.setFullName(name); + userData.setUserAuthorizationData(userAuthData); + + return userData; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/authorization/ServerAuthorization.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/authorization/ServerAuthorization.java index af7846db2..d58df4ddf 100644 --- a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/authorization/ServerAuthorization.java +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/authorization/ServerAuthorization.java @@ -33,6 +33,7 @@ import java.lang.reflect.Method; import java.math.BigInteger; import java.security.SecureRandom; +import java.text.SimpleDateFormat; import java.util.*; /** @@ -47,7 +48,7 @@ public class ServerAuthorization extends Authorization /** * Authentication service path in auth-server. */ - private static final String AUTHENTICATION_SERVICE_PATH = "/oidc-authn/oic"; + private static final String AUTHENTICATION_SERVICE_PATH = "/oidc"; /** * User web service path in auth-server. @@ -206,6 +207,12 @@ protected UserData onGetUserDataByAccessToken(String accessToken) return ROOT_USER_DATA; } + Optional reservationDevice = authorization.getReservationDeviceByToken(accessToken); + + if (reservationDevice.isPresent()) { + return reservationDevice.get().getUserData(); + } + Exception errorException = null; String errorReason = null; try { @@ -993,10 +1000,10 @@ private T handleAuthorizationRequestError(Exception exception) private static UserData createUserDataFromWebServiceData(JsonNode data) { // Required fields - if (!data.has("id")) { + if (!data.has("sub")) { throw new IllegalArgumentException("User data must contain identifier."); } - if (!data.has("first_name") || !data.has("last_name")) { + if (!data.has("given_name") || !data.has("family_name")) { throw new IllegalArgumentException("User data must contain given and family name."); } @@ -1004,23 +1011,23 @@ private static UserData createUserDataFromWebServiceData(JsonNode data) // Common user data UserInformation userInformation = userData.getUserInformation(); - userInformation.setUserId(data.get("id").asText()); - userInformation.setFirstName(data.get("first_name").asText()); - userInformation.setLastName(data.get("last_name").asText()); + userInformation.setUserId(data.get("sub").asText()); + userInformation.setFirstName(data.get("given_name").asText()); + userInformation.setLastName(data.get("family_name").asText()); if (data.has("organization")) { JsonNode organization = data.get("organization"); if (!organization.isNull()) { userInformation.setOrganization(organization.asText()); } } - if (data.has("mail")) { - JsonNode email = data.get("mail"); + if (data.has("email")) { + JsonNode email = data.get("email"); if (!email.isNull()) { userInformation.setEmail(email.asText()); } } - if (data.has("principal_names")) { - Iterator principalNameIterator = data.get("principal_names").elements(); + if (data.has("voperson_external_id")) { + Iterator principalNameIterator = data.get("voperson_external_id").elements(); while (principalNameIterator.hasNext()) { JsonNode principalName = principalNameIterator.next(); userInformation.addPrincipalName(principalName.asText()); @@ -1028,8 +1035,8 @@ private static UserData createUserDataFromWebServiceData(JsonNode data) } // Additional user data - if (data.has("language")) { - JsonNode language = data.get("language"); + if (data.has("locale")) { + JsonNode language = data.get("locale"); if (!language.isNull()) { Locale locale = new Locale(language.asText()); userData.setLocale(locale); @@ -1061,6 +1068,21 @@ private static UserData createUserDataFromWebServiceData(JsonNode data) } } + // for OpenID Connect + if (data.has("isCesnetEligibleLastSeen")) { + DateTime dateTime; + ObjectMapper mapper = new ObjectMapper(); + mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")); + try { + String date = data.get("isCesnetEligibleLastSeen").asText(); + dateTime = new DateTime(mapper.readValue("\"" + date + "\"", Date.class)); + } + catch (IOException e) { + throw new RuntimeException(e); + } + userData.setUserAuthorizationData( + new UserAuthorizationData(UserAuthorizationData.getLoaFromDate(dateTime))); + } // for AuthN Server v0.6.4 and newer if (data.has("authn_provider") && data.has("authn_instant") && data.has("loa")) { long instant = Long.valueOf(data.get("authn_instant").asText()) * 1000; diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/authorization/UserAuthorizationData.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/authorization/UserAuthorizationData.java index 53cdec1f4..add3b74ff 100644 --- a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/authorization/UserAuthorizationData.java +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/authorization/UserAuthorizationData.java @@ -82,4 +82,14 @@ public int getLoa() { return loa; } + + public static int getLoaFromDate(DateTime dateTime) { + if (dateTime.isAfter(DateTime.now().minusYears(1))) { + return LOA_EXTENDED; + } + if (dateTime.isAfter(DateTime.now().minusYears(2))) { + return LOA_BASIC; + } + return LOA_NONE; + } } 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..3a45dc49f --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/AbstractReservationRequestAuxData.java @@ -0,0 +1,35 @@ +package cz.cesnet.shongo.controller.booking.request; + +import com.fasterxml.jackson.databind.JsonNode; +import cz.cesnet.shongo.controller.booking.Allocation; +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; + @ManyToOne + @JoinColumn(name = "reused_allocation_id") + @Access(AccessType.FIELD) + private Allocation reusedAllocation; +} 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..287784c80 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.api.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,77 @@ 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 reservationRequestId reservation request id 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(Long reservationRequestId, AuxDataFilter filter) + { + return getAuxData(reservationRequestId, 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 reservationRequestId reservation request id for which the data shall be merged + * @param filter filter for data desired + * @return merged data + */ + private List getAuxData(Long reservationRequestId, AuxDataFilter filter) + { + String queryString = "SELECT arr.tagName, rt.tag.type, arr.enabled, arr.data, rt.tag.data" + + " FROM AbstractReservationRequestAuxData arr" + + " LEFT OUTER JOIN ResourceSpecification res_spec ON res_spec.id = arr.specification.id" + + // Needed for reused allocation (capacity) + " LEFT OUTER JOIN AbstractReservationRequest reusedRequest ON reusedRequest.id = arr.reusedAllocation.reservationRequest.id" + + " LEFT OUTER JOIN RoomSpecification room_spec ON room_spec.id = reusedRequest.specification.id OR room_spec.id = arr.specification.id" + + " JOIN ResourceTag rt ON rt.resource.id = res_spec.resource.id OR rt.resource.id = room_spec.deviceResource.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", reservationRequestId); + 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..4035862d9 --- /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.getData()); + return auxData; + } +} 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..5e94570e0 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/booking/request/auxdata/tagdata/TagData.java @@ -0,0 +1,75 @@ +package cz.cesnet.shongo.controller.booking.request.auxdata.tagdata; + +import com.fasterxml.jackson.databind.ObjectMapper; +import cz.cesnet.shongo.TodoImplementException; +import cz.cesnet.shongo.controller.api.AuxDataFilter; +import cz.cesnet.shongo.controller.booking.request.auxdata.AuxDataMerged; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public abstract class TagData +{ + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + 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; + } + + public static cz.cesnet.shongo.controller.api.TagData toApi(TagData tagData) + { + cz.cesnet.shongo.controller.api.TagData tagDataApi = new cz.cesnet.shongo.controller.api.TagData<>(); + tagDataApi.setName(tagData.getAuxData().getTagName()); + tagDataApi.setType(tagData.getAuxData().getType()); + tagDataApi.setData(objectMapper.valueToTree(tagData.getData())); + return tagDataApi; + } + + public cz.cesnet.shongo.controller.api.TagData toApi() + { + return toApi(this); + } +} 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..a56e37112 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,65 @@ package cz.cesnet.shongo.controller.booking.resource; +import com.fasterxml.jackson.databind.JsonNode; 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; /** * @author: Ondřej Pavelka */ @Entity public class Tag extends SimplePersistentObject { + 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 +74,8 @@ public void toApi(cz.cesnet.shongo.controller.api.Tag tagApi) { tagApi.setId(ObjectIdentifier.formatId(this)); tagApi.setName(name); + tagApi.setType(type); + tagApi.setData(data); } /** @@ -53,6 +92,7 @@ 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()); + this.setData(tagApi.getData()); } - } diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/domains/DomainAuthentication.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/domains/DomainAuthentication.java index c422e6d45..7941db6fb 100644 --- a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/domains/DomainAuthentication.java +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/domains/DomainAuthentication.java @@ -47,17 +47,17 @@ public DomainAuthentication(ControllerConfiguration configuration, this.domainService = domainService; this.notifier = notifier; try { - KeyStore keyStore = KeyStore.getInstance(configuration.getInterDomainSslKeyStoreType()); - FileInputStream keyStoreFile = new FileInputStream(configuration.getInterDomainSslKeyStore()); - keyStore.load(keyStoreFile, configuration.getInterDomainSslKeyStorePassword().toCharArray()); + KeyStore keyStore = KeyStore.getInstance(configuration.getRESTApiSslKeyStoreType()); + FileInputStream keyStoreFile = new FileInputStream(configuration.getRESTApiSslKeyStore()); + keyStore.load(keyStoreFile, configuration.getRESTApiSslKeyStorePassword().toCharArray()); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509"); - keyManagerFactory.init(keyStore, configuration.getInterDomainSslKeyStorePassword().toCharArray()); + keyManagerFactory.init(keyStore, configuration.getRESTApiSslKeyStorePassword().toCharArray()); this.keyManagerFactory = keyManagerFactory; } catch (GeneralSecurityException e) { - throw new RuntimeException("Failed to load keystore " + configuration.getInterDomainSslKeyStore(), e); + throw new RuntimeException("Failed to load keystore " + configuration.getRESTApiSslKeyStore(), e); } catch (IOException e) { e.printStackTrace(); - throw new RuntimeException("Failed to read keystore " + configuration.getInterDomainSslKeyStore(), e); + throw new RuntimeException("Failed to read keystore " + configuration.getRESTApiSslKeyStore(), 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..5875524e8 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.api.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,33 @@ private ReservationNotification(Reservation reservation, } } + private void addRecipientsFromNotificationTags(AbstractReservationRequest reservationRequest, + ReservationRequestManager reservationRequestManager) + { + AuxDataFilter filter = new AuxDataFilter(); + filter.setTagType(TagType.NOTIFY_EMAIL); + filter.setEnabled(true); + + List notifyEmailAuxData = reservationRequestManager.getTagData(reservationRequest.getId(), 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 +374,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 +409,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 +426,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/rest/CacheProvider.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/CacheProvider.java new file mode 100644 index 000000000..6408a5df1 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/CacheProvider.java @@ -0,0 +1,110 @@ +package cz.cesnet.shongo.controller.rest; + +import cz.cesnet.shongo.api.UserInformation; +import cz.cesnet.shongo.controller.api.Executable; +import cz.cesnet.shongo.controller.api.Group; +import cz.cesnet.shongo.controller.api.Reservation; +import cz.cesnet.shongo.controller.api.ReservationRequestSummary; +import cz.cesnet.shongo.controller.api.ResourceSummary; +import cz.cesnet.shongo.controller.api.SecurityToken; + +/** + * {@link RestCache} provided for specified {@link #securityToken}. + * + * @author Martin Srom + */ +public class CacheProvider +{ + /** + * {@link RestCache} to be used for retrieving {@link UserInformation}. + */ + private final RestCache cache; + + /** + * {@link SecurityToken} to be used for retrieving {@link UserInformation} by the {@link #cache}. + */ + private final SecurityToken securityToken; + + /** + * Constructor. + * + * @param cache sets the {@link #cache} + * @param securityToken sets the {@link #securityToken} + */ + public CacheProvider(RestCache cache, SecurityToken securityToken) + { + this.cache = cache; + this.securityToken = securityToken; + } + + /** + * @return {@link #securityToken} + */ + public SecurityToken getSecurityToken() + { + return securityToken; + } + + /** + * @param userId + * @return {@link UserInformation} for given {@code userId} + */ + public UserInformation getUserInformation(String userId) + { + return cache.getUserInformation(securityToken, userId); + } + + /** + * @param groupId + * @return {@link Group} for given {@code groupId} + */ + public Group getGroup(String groupId) + { + return cache.getGroup(securityToken, groupId); + } + + /** + * @param resourceId + * @return {@link ResourceSummary} for given {@code resourceId} + */ + public ResourceSummary getResourceSummary(String resourceId) + { + return cache.getResourceSummary(securityToken, resourceId); + } + + /** + * @param reservationRequestId + * @return {@link ReservationRequestSummary} for given {@code reservationRequestId} + */ + public ReservationRequestSummary getAllocatedReservationRequestSummary(String reservationRequestId) + { + return cache.getAllocatedReservationRequestSummary(securityToken, reservationRequestId); + } + + /** + * @param reservationId + * @return {@link Reservation} for given {@code reservationId} + */ + public Reservation getReservation(String reservationId) + { + return cache.getReservation(securityToken, reservationId); + } + + /** + * @param executable + * @return identifier of reservation request for given {@code executable} + */ + public String getReservationRequestIdByExecutable(Executable executable) + { + return cache.getReservationRequestIdByExecutable(securityToken, executable); + } + + /** + * @param executableId + * @return {@link Executable} for given {@code executableId} + */ + public Executable getExecutable(String executableId) + { + return cache.getExecutable(securityToken, executableId); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/ErrorHandler.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/ErrorHandler.java new file mode 100644 index 000000000..195e982a4 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/ErrorHandler.java @@ -0,0 +1,53 @@ +package cz.cesnet.shongo.controller.rest; + +import cz.cesnet.shongo.controller.Controller; +import cz.cesnet.shongo.controller.ControllerConfiguration; +import cz.cesnet.shongo.controller.EmailSender; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.mail.MessagingException; +import java.util.Collection; + +/** + * Error handler. + * + * @author Martin Srom + * @author Filip Karnis + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ErrorHandler +{ + + private final Controller controller; + private final ControllerConfiguration configuration; + + /** + * Send email to administrators. + * + * @param replyTo + * @param subject + * @param content + * @return result + */ + public void sendEmailToAdministrator(String replyTo, String subject, String content) throws MessagingException + { + Collection administratorEmails = configuration.getAdministratorEmails(); + if (administratorEmails.size() == 0) { + log.warn("Administrator email for sending error reports is not configured."); + return; + } + + try { + EmailSender.Email email = new EmailSender.Email(administratorEmails, replyTo, subject, content); + controller.getEmailSender().sendEmail(email); + } + catch (MessagingException e) { + log.error("Failed to send email '" + subject + "':\n" + content, e); + throw e; + } + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/RESTApiServer.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/RESTApiServer.java new file mode 100644 index 000000000..5cc9c7450 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/RESTApiServer.java @@ -0,0 +1,138 @@ +package cz.cesnet.shongo.controller.rest; + +import com.google.common.base.Strings; +import cz.cesnet.shongo.controller.ControllerConfiguration; +import cz.cesnet.shongo.controller.domains.BasicAuthFilter; +import cz.cesnet.shongo.controller.domains.SSLClientCertFilter; +import cz.cesnet.shongo.ssl.ConfiguredSSLContext; +import cz.cesnet.shongo.ssl.SSLCommunication; +import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.springframework.web.context.ContextLoaderListener; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.filter.DelegatingFilterProxy; +import org.springframework.web.servlet.DispatcherServlet; + +import javax.servlet.DispatcherType; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.EnumSet; + +public class RESTApiServer +{ + private static final String SERVLET_PATH = "/"; + private static final String SERVLET_NAME = "rest-api"; + private static final String INTER_DOMAIN_API_PATH = "/domain/**"; + + public static Server start(ControllerConfiguration configuration) + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException + { + Server restServer = new Server(); + ConfiguredSSLContext.getInstance().loadConfiguration(configuration); + + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.scan("cz.cesnet.shongo.controller"); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.addEventListener(new ContextLoaderListener(context)); + + ServletHolder servletHolder = new ServletHolder(SERVLET_NAME, new DispatcherServlet(context)); + servletContextHandler.addServlet(servletHolder, SERVLET_PATH); + + FilterHolder springSecurityFilter = new FilterHolder(new DelegatingFilterProxy("springSecurityFilterChain")); + servletContextHandler.addFilter(springSecurityFilter, "/*", EnumSet.allOf(DispatcherType.class)); + + ServerConnector httpsConnector = createHTTPConnector(configuration, servletContextHandler, restServer); + + restServer.setConnectors(new Connector[]{httpsConnector}); + restServer.setHandler(servletContextHandler); + + try { + restServer.start(); + } + catch (Exception exception) { + throw new RuntimeException(exception); + } + + return restServer; + } + + private static ServerConnector createHTTPConnector( + ControllerConfiguration configuration, + ServletContextHandler contextHandler, + Server server) + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException + { + final HttpConfiguration http_config = new HttpConfiguration(); + http_config.setSecurePort(configuration.getRESTApiPort()); + ServerConnector serverConnector; + + final String sslKeyStore = configuration.getRESTApiSslKeyStore(); + if (sslKeyStore != null) { + http_config.setSecureScheme(HttpScheme.HTTPS.asString()); + + final HttpConfiguration https_config = new HttpConfiguration(http_config); + https_config.addCustomizer(new SecureRequestCustomizer()); + + final SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + + sslContextFactory.setKeyStorePath(sslKeyStore); + sslContextFactory.setKeyStorePassword(configuration.getRESTApiSslKeyStorePassword()); + String keystoreType = configuration.getRESTApiSslKeyStoreType(); + if (!Strings.isNullOrEmpty(keystoreType)) { + sslContextFactory.setKeyStoreType(configuration.getRESTApiSslKeyStoreType()); + } + + if (configuration.isInterDomainConfigured()) { + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(null); + // Load certificates of foreign domain's CAs + for (String certificatePath : configuration.getForeignDomainsCaCertFiles()) { + trustStore.setCertificateEntry(certificatePath.substring(0, certificatePath.lastIndexOf('.')), + SSLCommunication.readPEMCert(certificatePath)); + } + sslContextFactory.setTrustStore(trustStore); + + if (configuration.requiresClientPKIAuth()) { + // Enable forced client auth + sslContextFactory.setNeedClientAuth(true); + // Enable SSL client filter by certificates + EnumSet filterTypes = EnumSet.of(DispatcherType.REQUEST); + contextHandler.addFilter(SSLClientCertFilter.class, INTER_DOMAIN_API_PATH, filterTypes); + } + else { + EnumSet filterTypes = EnumSet.of(DispatcherType.REQUEST); + contextHandler.addFilter(BasicAuthFilter.class, INTER_DOMAIN_API_PATH, filterTypes); + } + } + serverConnector = new ServerConnector(server, + new SslConnectionFactory(sslContextFactory, "http/1.1"), + new HttpConnectionFactory(https_config)); + } + else { + http_config.setSecureScheme(HttpScheme.HTTP.asString()); + serverConnector = new ServerConnector(server, new HttpConnectionFactory(http_config)); + } + + String host = configuration.getRESTApiHost(); + if (!Strings.isNullOrEmpty(host)) { + serverConnector.setHost(host); + } + serverConnector.setPort(configuration.getRESTApiPort()); + + return serverConnector; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/RestApiPath.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/RestApiPath.java new file mode 100644 index 000000000..a2f4eb53e --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/RestApiPath.java @@ -0,0 +1,66 @@ +package cz.cesnet.shongo.controller.rest; + +/** + * URL paths for REST endpoints. + * + * @author Filip Karnis + */ +public class RestApiPath +{ + + public static final String API_PREFIX = "/api/v1"; + public static final String ID_SUFFIX = "/{id:.+}"; + public static final String ENTITY_SUFFIX = "/{entityId:.+}"; + + public static final String REPORT = API_PREFIX + "/report"; + + // Users and groups + public static final String USERS_AND_GROUPS = API_PREFIX; + public static final String SETTINGS = "/settings"; + public static final String USERS_LIST = "/users"; + public static final String USERS_DETAIL = "/users/{userId:.+}"; + public static final String GROUPS_LIST = "/groups"; + public static final String GROUPS_DETAIL = "/groups/{groupId:.+}"; + + // Tags + public static final String TAGS = API_PREFIX + "/tags"; + + // Resources + public static final String RESOURCES = API_PREFIX + "/resources"; + public static final String CAPACITY_UTILIZATION = "/capacity_utilization"; + public static final String CAPACITY_UTILIZATION_DETAIL = "/{id}/capacity_utilization"; + + // Reservation requests + public static final String RESERVATION_REQUESTS = API_PREFIX + "/reservation_requests"; + public static final String RESERVATION_REQUESTS_AWAITING_CONFIRMATION = "/awaiting_confirmation"; + public static final String RESERVATION_REQUESTS_ACCEPT = ID_SUFFIX + "/accept"; + public static final String RESERVATION_REQUESTS_REJECT = ID_SUFFIX + "/reject"; + public static final String RESERVATION_REQUESTS_REVERT = ID_SUFFIX + "/revert"; + public static final String RESERVATION_REQUESTS_AUX_DATA = ID_SUFFIX + "/tag_data"; + + // Participants + public static final String PARTICIPANTS = API_PREFIX + "/reservation_requests/{id:.+}/participants"; + public static final String PARTICIPANTS_ID_SUFFIX = "/{participantId:.+}"; + + // Roles + public static final String ROLES = API_PREFIX + "/reservation_requests/{id:.+}/roles"; + + // Recordings + public static final String RECORDINGS = API_PREFIX + "/reservation_requests/{id:.+}/recordings"; + public static final String RECORDINGS_ID_SUFFIX = "/{recordingId:.+}"; + + // Rooms + public static final String ROOMS = API_PREFIX + "/rooms"; + + // Runtime management + public static final String RUNTIME_MANAGEMENT = API_PREFIX + "/reservation_requests/{id:.+}/runtime_management"; + public static final String RUNTIME_MANAGEMENT_PARTICIPANTS = "/participants"; + public static final String RUNTIME_MANAGEMENT_PARTICIPANTS_MODIFY = "/participants/{participantId:.+}"; + public static final String RUNTIME_MANAGEMENT_PARTICIPANTS_DISCONNECT = "/participants/{participantId:.+}/disconnect"; + public static final String RUNTIME_MANAGEMENT_PARTICIPANTS_SNAPSHOT = "/participants/{participantId:.+}/snapshot"; + public static final String RUNTIME_MANAGEMENT_RECORDING_START = "/recording/start"; + public static final String RUNTIME_MANAGEMENT_RECORDING_STOP = "/recording/stop"; + + // Reservation device + public static final String RESERVATION_DEVICE = API_PREFIX + "/reservation_device"; +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/RestCache.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/RestCache.java new file mode 100644 index 000000000..d285f5b20 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/RestCache.java @@ -0,0 +1,715 @@ +package cz.cesnet.shongo.controller.rest; + +import cz.cesnet.shongo.CommonReportSet; +import cz.cesnet.shongo.ExpirationMap; +import cz.cesnet.shongo.TodoImplementException; +import cz.cesnet.shongo.api.UserInformation; +import cz.cesnet.shongo.controller.ControllerReportSet; +import cz.cesnet.shongo.controller.ObjectPermission; +import cz.cesnet.shongo.controller.SystemPermission; +import cz.cesnet.shongo.controller.api.AllocationState; +import cz.cesnet.shongo.controller.api.Executable; +import cz.cesnet.shongo.controller.api.Group; +import cz.cesnet.shongo.controller.api.ObjectPermissionSet; +import cz.cesnet.shongo.controller.api.Reservation; +import cz.cesnet.shongo.controller.api.ReservationRequestSummary; +import cz.cesnet.shongo.controller.api.Resource; +import cz.cesnet.shongo.controller.api.ResourceSummary; +import cz.cesnet.shongo.controller.api.SecurityToken; +import cz.cesnet.shongo.controller.api.request.GroupListRequest; +import cz.cesnet.shongo.controller.api.request.ListResponse; +import cz.cesnet.shongo.controller.api.request.ObjectPermissionListRequest; +import cz.cesnet.shongo.controller.api.request.ReservationRequestListRequest; +import cz.cesnet.shongo.controller.api.request.ResourceListRequest; +import cz.cesnet.shongo.controller.api.request.UserListRequest; +import cz.cesnet.shongo.controller.api.rpc.AuthorizationService; +import cz.cesnet.shongo.controller.api.rpc.ExecutableService; +import cz.cesnet.shongo.controller.api.rpc.ReservationService; +import cz.cesnet.shongo.controller.api.rpc.ResourceService; +import cz.cesnet.shongo.controller.rest.error.ObjectInaccessibleException; +import cz.cesnet.shongo.controller.rest.models.resource.ResourcesUtilization; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.joda.time.DateTime; +import org.joda.time.Duration; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Cache of {@link UserInformation}s, {@link ObjectPermission}s, {@link ReservationRequestSummary}s. + * + * @author Filip Karnis + * @author Martin Srom + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class RestCache +{ + + /** + * Expiration of user information/permissions in minutes. + */ + private static final long USER_EXPIRATION_MINUTES = 5; + + private final AuthorizationService authorizationService; + + private final ResourceService resourceService; + + private final ReservationService reservationService; + + private final ExecutableService executableService; + + /** + * @see ResourcesUtilization + */ + private final ExpirationMap resourcesUtilizationByToken = + new ExpirationMap<>(Duration.standardMinutes(10)); + + /** + * {@link UserInformation}s by {@link SecurityToken}. + */ + private final ExpirationMap> userPermissionsByToken = + new ExpirationMap<>(Duration.standardMinutes(5)); + + /** + * {@link UserInformation}s by user-ids. + */ + private final ExpirationMap userInformationByUserId = + new ExpirationMap<>(Duration.standardMinutes(USER_EXPIRATION_MINUTES)); + + /** + * {@link Group}s by group-ids. + */ + private final ExpirationMap groupByGroupId = + new ExpirationMap<>(Duration.standardMinutes(USER_EXPIRATION_MINUTES)); + + /** + * {@link UserState}s by {@link SecurityToken}. + */ + private final ExpirationMap userStateByToken = + new ExpirationMap<>(Duration.standardHours(1)); + /** + * {@link ResourceSummary} by identifier. + */ + private final ExpirationMap resourceSummaryById = + new ExpirationMap<>(Duration.standardHours(1)); + /** + * {@link Resource} by identifier. + */ + private final ExpirationMap resourceById = + new ExpirationMap<>(Duration.standardHours(1)); + /** + * {@link ReservationRequestSummary} by identifier. + */ + private final ExpirationMap reservationRequestById = + new ExpirationMap<>(Duration.standardMinutes(5)); + + /** + * {@link Reservation} by identifier. + */ + private final ExpirationMap reservationById = + new ExpirationMap<>(Duration.standardMinutes(5)); + + /** + * Ids of resources with public calendar by their calendarUriKey + */ + private final ExpirationMap resourceIdsWithPublicCalendarByUriKey = + new ExpirationMap<>(Duration.standardMinutes(10)); + + /** + * {@link Reservation} by identifier. + */ + private final ExpirationMap executableById = + new ExpirationMap<>(Duration.standardSeconds(10)); + + /** + * @param userId + * @return {@link UserInformation} for not existing user with given {@code userId} + */ + private static UserInformation createNotExistingUserInformation(String userId) + { + UserInformation userInformation = new UserInformation(); + userInformation.setUserId(userId); + userInformation.setFirstName("Non-Existent-User"); + userInformation.setLastName("(" + userId + ")"); + return userInformation; + } + + /** + * Method called each 5 minutes to clear expired items. + */ + @Scheduled(fixedDelay = (USER_EXPIRATION_MINUTES * 60 * 1000)) + public synchronized void clearExpired() + { + log.debug("Clearing expired user cache..."); + DateTime dateTimeNow = DateTime.now(); + userPermissionsByToken.clearExpired(dateTimeNow); + userInformationByUserId.clearExpired(dateTimeNow); + userStateByToken.clearExpired(dateTimeNow); + for (UserState userState : userStateByToken) { + userState.objectPermissionsByObject.clearExpired(dateTimeNow); + } + resourceSummaryById.clearExpired(dateTimeNow); + resourceById.clearExpired(dateTimeNow); + reservationRequestById.clearExpired(dateTimeNow); + reservationById.clearExpired(dateTimeNow); + executableById.clearExpired(dateTimeNow); + resourcesUtilizationByToken.clearExpired(dateTimeNow); + resourceIdsWithPublicCalendarByUriKey.clearExpired(dateTimeNow); + } + + /** + * @param executableId to be removed from the {@link #executableById} + */ + public synchronized void clearExecutable(String executableId) + { + executableById.remove(executableId); + } + + /** + * @param securityToken to be removed from the {@link #userPermissionsByToken} + */ + public synchronized void clearUserPermissions(SecurityToken securityToken) + { + userPermissionsByToken.remove(securityToken); + } + + /** + * @param securityToken + * @return list of {@link SystemPermission} that requesting user has + */ + public synchronized List getSystemPermissions(SecurityToken securityToken) + { + List userPermissions = userPermissionsByToken.get(securityToken); + if (userPermissions != null) { + return userPermissions; + } + userPermissions = Stream.of(SystemPermission.values()).filter(permission -> + authorizationService.hasSystemPermission(securityToken, permission) + ).collect(Collectors.toList()); + userPermissionsByToken.put(securityToken, userPermissions); + return userPermissions; + } + + /** + * @param securityToken to be used for fetching the {@link UserInformation} + * @param userId user-id of the requested user + * @return {@link UserInformation} for given {@code userId} + */ + public synchronized UserInformation getUserInformation(SecurityToken securityToken, String userId) + { + if (userId == null) { + return null; + } + UserInformation userInformation = userInformationByUserId.get(userId); + if (userInformation != null) { + return userInformation; + } + try { + ListResponse response = authorizationService.listUsers( + new UserListRequest(securityToken, userId)); + if (response.getCount() == 0) { + throw new ControllerReportSet.UserNotExistsException(userId); + } + userInformation = response.getItem(0); + } + catch (ControllerReportSet.UserNotExistsException exception) { + log.warn("User with id '" + userId + "' doesn't exist.", exception); + userInformation = createNotExistingUserInformation(userId); + } + userInformationByUserId.put(userId, userInformation); + return userInformation; + } + + /** + * @param securityToken to be used for fetching the {@link UserInformation}s + * @param userIds user-ids of the requested users + */ + public synchronized void fetchUserInformation(SecurityToken securityToken, Collection userIds) + { + Set missingUserIds = null; + for (String userId : userIds) { + if (!userInformationByUserId.contains(userId)) { + if (missingUserIds == null) { + missingUserIds = new HashSet<>(); + } + missingUserIds.add(userId); + } + } + if (missingUserIds != null) { + while (missingUserIds.size() > 0) { + try { + ListResponse response = authorizationService.listUsers( + new UserListRequest(securityToken, missingUserIds)); + for (UserInformation userInformation : response.getItems()) { + String userId = userInformation.getUserId(); + userInformationByUserId.put(userId, userInformation); + missingUserIds.remove(userId); + } + if (missingUserIds.size() > 0) { + throw new ControllerReportSet.UserNotExistsException(missingUserIds.iterator().next()); + } + } + catch (ControllerReportSet.UserNotExistsException exception) { + String userId = exception.getUser(); + log.warn("User with id '" + userId + "' doesn't exist.", exception); + UserInformation userInformation = createNotExistingUserInformation(userId); + userInformationByUserId.put(userId, userInformation); + missingUserIds.remove(userId); + } + } + } + } + + /** + * @param securityToken to be used for fetching the {@link Group} + * @param groupId group-id of the requested group + * @return {@link Group} for given {@code groupId} + */ + public synchronized Group getGroup(SecurityToken securityToken, String groupId) + { + Group group = groupByGroupId.get(groupId); + if (group == null) { + ListResponse response = authorizationService.listGroups( + new GroupListRequest(securityToken, groupId)); + if (response.getCount() == 0) { + throw new ControllerReportSet.GroupNotExistsException(groupId); + } + group = response.getItem(0); + groupByGroupId.put(groupId, group); + } + return group; + } + + /** + * @param securityToken + * @return {@link UserState} for user with given {@code securityToken} + */ + private synchronized UserState getUserState(SecurityToken securityToken) + { + UserState userState = userStateByToken.get(securityToken); + if (userState == null) { + userState = new UserState(); + userStateByToken.put(securityToken, userState); + } + return userState; + } + + /** + * @param securityToken of the requesting user + * @param objectId of the object + * @return set of {@link ObjectPermission} for requesting user and given {@code objectId} + */ + public synchronized Set getObjectPermissions(SecurityToken securityToken, String objectId) + { + UserState userState = getUserState(securityToken); + Set objectPermissions = userState.objectPermissionsByObject.get(objectId); + if (objectPermissions == null) { + Map permissionsByObject = authorizationService.listObjectPermissions( + new ObjectPermissionListRequest(securityToken, objectId)); + objectPermissions = new HashSet<>(permissionsByObject.get(objectId).getObjectPermissions()); + userState.objectPermissionsByObject.put(objectId, objectPermissions); + } + return objectPermissions; + } + + /** + * @param securityToken + * @param reservationRequests + * @return map of {@link ObjectPermission}s by reservation request identifier + */ + public Map> getReservationRequestsPermissions(SecurityToken securityToken, + Collection reservationRequests) + { + Map> permissionsByReservationRequestId = new HashMap<>(); + Set reservationRequestIds = new HashSet<>(); + for (ReservationRequestSummary reservationRequest : reservationRequests) { + String reservationRequestId = reservationRequest.getId(); + Set objectPermissions = + getObjectPermissionsWithoutFetching(securityToken, reservationRequestId); + if (objectPermissions != null) { + permissionsByReservationRequestId.put(reservationRequestId, objectPermissions); + } + else { + reservationRequestIds.add(reservationRequestId); + } + } + if (reservationRequestIds.size() > 0) { + permissionsByReservationRequestId.putAll(fetchObjectPermissions(securityToken, reservationRequestIds)); + } + return permissionsByReservationRequestId; + } + + /** + * @param securityToken of the requesting user + * @param objectId of the object + * @return set of {@link ObjectPermission} for requesting user and given {@code objectId} + * or null if the {@link ObjectPermission}s aren't cached + */ + public synchronized Set getObjectPermissionsWithoutFetching( + SecurityToken securityToken, String objectId) + { + UserState userState = getUserState(securityToken); + return userState.objectPermissionsByObject.get(objectId); + } + + /** + * Fetch {@link ObjectPermission}s for given {@code objectIds}. + * + * @param securityToken + * @param objectIds + * @return fetched {@link ObjectPermission}s by {@code objectIds} + */ + public synchronized Map> fetchObjectPermissions( + SecurityToken securityToken, Set objectIds) + { + Map> result = new HashMap<>(); + if (objectIds.isEmpty()) { + return result; + } + UserState userState = getUserState(securityToken); + Map permissionsByObject = + authorizationService.listObjectPermissions(new ObjectPermissionListRequest(securityToken, objectIds)); + for (Map.Entry entry : permissionsByObject.entrySet()) { + String objectId = entry.getKey(); + Set objectPermissions = userState.objectPermissionsByObject.get(objectId); + if (objectPermissions == null) { + objectPermissions = new HashSet<>(); + userState.objectPermissionsByObject.put(objectId, objectPermissions); + } + objectPermissions.clear(); + objectPermissions.addAll(entry.getValue().getObjectPermissions()); + result.put(objectId, objectPermissions); + } + return result; + } + + public synchronized Resource getResource(SecurityToken securityToken, String resourceId) { + Resource resource = resourceById.get(resourceId); + if (resource == null) { + resource = resourceService.getResource(securityToken, resourceId); + resourceById.put(resourceId, resource); + } + return resource; + } + + /** + * @param securityToken to be used for fetching the {@link ResourceSummary}s + * @param resourceIds resource-ids to be fetched + */ + public synchronized void fetchResourceSummaries(SecurityToken securityToken, Collection resourceIds) + { + Set missingResourceIds = null; + for (String resourceId : resourceIds) { + if (!resourceSummaryById.contains(resourceId)) { + if (missingResourceIds == null) { + missingResourceIds = new HashSet(); + } + missingResourceIds.add(resourceId); + } + } + if (missingResourceIds != null) { + ResourceListRequest request = new ResourceListRequest(); + request.setSecurityToken(securityToken); + for (String resourceId : request.getResourceIds()) { + request.addResourceId(resourceId); + } + ListResponse response = resourceService.listResources(request); + for (ResourceSummary resource : response.getItems()) { + String resourceId = resource.getId(); + resourceSummaryById.put(resourceId, resource); + missingResourceIds.remove(resourceId); + } + if (missingResourceIds.size() > 0) { + throw new CommonReportSet.ObjectNotExistsException(ResourceSummary.class.getSimpleName(), + missingResourceIds.iterator().next()); + } + } + } + + /** + * @param securityToken + * @param resourceId + * @return {@link ResourceSummary} for given {@code resourceId} + */ + public ResourceSummary getResourceSummary(SecurityToken securityToken, String resourceId) + { + ResourceSummary resourceSummary = resourceSummaryById.get(resourceId); + if (resourceSummary == null) { + ResourceListRequest request = new ResourceListRequest(); + request.setSecurityToken(securityToken); + request.addResourceId(resourceId); + ListResponse response = resourceService.listResources(request); + if (response.getItemCount() == 1) { + resourceSummary = response.getItem(0); + resourceSummaryById.put(resourceSummary.getId(), resourceSummary); + } + } + if (resourceSummary == null) { + throw new CommonReportSet.ObjectNotExistsException(ResourceSummary.class.getSimpleName(), resourceId); + } + return resourceSummary; + } + + /** + * Load {@link ReservationRequestSummary}s for given {@code reservationRequestIds} to the {@link RestCache}. + * + * @param securityToken + * @param reservationRequestIds + */ + public synchronized void fetchReservationRequests(SecurityToken securityToken, Set reservationRequestIds) + { + Set missingReservationRequestIds = null; + for (String reservationRequestId : reservationRequestIds) { + if (!reservationRequestById.contains(reservationRequestId)) { + if (missingReservationRequestIds == null) { + missingReservationRequestIds = new HashSet<>(); + } + missingReservationRequestIds.add(reservationRequestId); + } + } + if (missingReservationRequestIds != null) { + ReservationRequestListRequest request = new ReservationRequestListRequest(); + request.setSecurityToken(securityToken); + request.setReservationRequestIds(missingReservationRequestIds); + ListResponse response = reservationService.listReservationRequests(request); + for (ReservationRequestSummary reservationRequest : response) { + if (reservationRequest.isAllowCache()) { + reservationRequestById.put(reservationRequest.getId(), reservationRequest); + } + } + } + } + + /** + * Retrieve {@link ReservationRequestSummary} from {@link RestCache} or from {@link #reservationService} + * if it doesn't exist in the {@link RestCache}. + * + * @param securityToken + * @param reservationRequestId + * @return {@link ReservationRequestSummary} for given {@code reservationRequestId} + */ + public synchronized ReservationRequestSummary getReservationRequestSummary(SecurityToken securityToken, + String reservationRequestId) + { + ReservationRequestSummary reservationRequest = reservationRequestById.get(reservationRequestId); + if (reservationRequest == null) { + reservationRequest = getReservationRequestSummaryNotCached(securityToken, reservationRequestId); + } + return reservationRequest; + } + + /** + * Similar as {@link #getReservationRequestSummary} but the {@link ReservationRequestSummary} is loaded from + * the {@link #reservationService} also when it is in {@link AllocationState#NOT_ALLOCATED} state (to update it). + * + * @param securityToken + * @param reservationRequestId + * @return {@link ReservationRequestSummary} for given {@code reservationRequestId} + */ + public synchronized ReservationRequestSummary getAllocatedReservationRequestSummary(SecurityToken securityToken, + String reservationRequestId) + { + ReservationRequestSummary reservationRequest = reservationRequestById.get(reservationRequestId); + if (reservationRequest == null || reservationRequest.getAllocatedReservationId() == null) { + reservationRequest = getReservationRequestSummaryNotCached(securityToken, reservationRequestId); + } + return reservationRequest; + } + + /** + * @param securityToken + * @param reservationRequestId + * @return {@link ReservationRequestSummary} for given {@code reservationRequestId} + */ + public synchronized ReservationRequestSummary getReservationRequestSummaryNotCached(SecurityToken securityToken, + String reservationRequestId) + { + ReservationRequestListRequest request = new ReservationRequestListRequest(); + request.setSecurityToken(securityToken); + request.addReservationRequestId(reservationRequestId); + ListResponse response = reservationService.listReservationRequests(request); + if (response.getItemCount() > 0) { + ReservationRequestSummary reservationRequest = response.getItem(0); + DateTime start = reservationRequest.getEarliestSlot().getStart(); + DateTime end = reservationRequest.getEarliestSlot().getEnd(); + final boolean startsSoon = start.isAfterNow() && start.isBefore(DateTime.now().plusMinutes(5)); + final boolean happensNow = start.isBeforeNow() && end.isAfterNow(); + if (reservationRequest.isAllowCache() && !(startsSoon || happensNow)) { + reservationRequestById.put(reservationRequest.getId(), reservationRequest); + } + return reservationRequest; + } + throw new ObjectInaccessibleException(reservationRequestId); + } + + /** + * @param securityToken + * @param reservationId + * @return {@link Reservation} for given {@code reservationId} + */ + public synchronized Reservation getReservation(SecurityToken securityToken, String reservationId) + { + Reservation reservation = reservationById.get(reservationId); + if (reservation == null) { + reservation = reservationService.getReservation(securityToken, reservationId); + reservationById.put(reservationId, reservation); + } + return reservation; + } + + /** + * @param securityToken + * @param executable + * @return reservation request id for given {@code executable} + */ + public synchronized String getReservationRequestIdByExecutable(SecurityToken securityToken, Executable executable) + { + Reservation reservation = getReservation(securityToken, executable.getReservationId()); + return reservation.getReservationRequestId(); + } + + /** + * @param securityToken + * @param objectId + * @return reservation request id for given {@code objectId} + */ + public synchronized String getReservationRequestId(SecurityToken securityToken, String objectId) + { + if (objectId.contains(":req:")) { + return objectId; + } + else if (objectId.contains(":rsv:")) { + Reservation reservation = getReservation(securityToken, objectId); + return reservation.getReservationRequestId(); + } + else if (objectId.contains(":exe:")) { + Executable executable = getExecutable(securityToken, objectId); + return getReservationRequestIdByExecutable(securityToken, executable); + } + else { + throw new TodoImplementException(objectId); + } + } + + /** + * @param securityToken + * @param objectId + * @return executable id for given {@code objectId} + */ + public String getExecutableId(SecurityToken securityToken, String objectId) + { + if (objectId.contains(":exe:")) { + return objectId; + } + + if (objectId.contains(":req:")) { + ReservationRequestSummary request = getAllocatedReservationRequestSummary(securityToken, objectId); + String reservationId = request.getAllocatedReservationId(); + if (reservationId == null) { + log.info("Reservation doesn't exist."); + return null; + } + objectId = reservationId; + } + + if (objectId.contains(":rsv:")) { + Reservation reservation = getReservation(securityToken, objectId); + Executable executable = reservation.getExecutable(); + if (executable == null) { + log.info("Reservation " + objectId + " doesn't have executable."); + return null; + } + return executable.getId(); + } + + throw new TodoImplementException(objectId); + } + + /** + * @param securityToken + * @param executableId + * @return {@link Executable} for given {@code executableId} + */ + public synchronized Executable getExecutable(SecurityToken securityToken, String executableId) + { + Executable executable = executableById.get(executableId); + if (executable == null) { + executable = executableService.getExecutable(securityToken, executableId); + executableById.put(executableId, executable); + } + return executable; + } + + /** + * @param securityToken for which the {@link ResourcesUtilization} shall be returned + * @param forceRefresh specifies whether a fresh version should be returned + * @return {@link ResourcesUtilization} for given {@code securityToken} + */ + public ResourcesUtilization getResourcesUtilization(SecurityToken securityToken, boolean forceRefresh) + { + synchronized (resourcesUtilizationByToken) { + ResourcesUtilization resourcesUtilization = resourcesUtilizationByToken.get(securityToken); + if (resourcesUtilization == null || forceRefresh) { + resourcesUtilization = new ResourcesUtilization( + securityToken, reservationService, resourceService, this + ); + resourcesUtilizationByToken.put(securityToken, resourcesUtilization); + } + return resourcesUtilization; + } + } + + /** + * Returns resource's ID for given uriKey. + * Accessible resource IDs are cached. + * + * @param uriKey + * @return resource's reservations iCalendar text for export + */ + public String getResourceIdWithUriKey(String uriKey) + { + + resourceIdsWithPublicCalendarByUriKey.clearExpired(DateTime.now()); + //Check if resource really exists + if (resourceIdsWithPublicCalendarByUriKey.size() == 0) { + for (ResourceSummary resourceSummary : resourceService.getResourceIdsWithPublicCalendar()) { + resourceIdsWithPublicCalendarByUriKey.put(resourceSummary.getCalendarUriKey(), resourceSummary.getId()); + } + } + if (!resourceIdsWithPublicCalendarByUriKey.contains(uriKey)) { + return null; + } + return resourceIdsWithPublicCalendarByUriKey.get(uriKey); + } + + /** + * Cached information for single user. + */ + private static class UserState + { + /** + * Set of permissions which the user has for object. + */ + private final ExpirationMap> objectPermissionsByObject = + new ExpirationMap<>(); + + /** + * Constructor. + */ + public UserState() + { + objectPermissionsByObject.setExpiration(Duration.standardMinutes(USER_EXPIRATION_MINUTES)); + } + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/RoomCache.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/RoomCache.java new file mode 100644 index 000000000..27fc9f34f --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/RoomCache.java @@ -0,0 +1,285 @@ +package cz.cesnet.shongo.controller.rest; + +import cz.cesnet.shongo.CommonReportSet; +import cz.cesnet.shongo.ExpirationMap; +import cz.cesnet.shongo.api.MediaData; +import cz.cesnet.shongo.api.Room; +import cz.cesnet.shongo.api.RoomParticipant; +import cz.cesnet.shongo.controller.api.Executable; +import cz.cesnet.shongo.controller.api.RoomExecutable; +import cz.cesnet.shongo.controller.api.SecurityToken; +import cz.cesnet.shongo.controller.api.rpc.ResourceControlService; +import cz.cesnet.shongo.controller.rest.error.UnsupportedApiException; +import lombok.RequiredArgsConstructor; +import org.joda.time.Duration; +import org.springframework.stereotype.Service; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Cache of information for management of rooms. + * + * @author Martin Srom + */ +@Service +@RequiredArgsConstructor +public class RoomCache +{ + + private final ResourceControlService resourceControlService; + + private final RestCache cache; + + /** + * {@link RoomExecutable} by roomExecutableId. + */ + private final ExpirationMap roomExecutableCache = + new ExpirationMap<>(Duration.standardSeconds(15)); + + /** + * {@link Room} by roomExecutableId". + */ + private final ExpirationMap roomCache = + new ExpirationMap<>(Duration.standardSeconds(30)); + + /** + * Collection of {@link RoomParticipant}s by roomExecutableId. + */ + private final ExpirationMap> roomParticipantsCache = + new ExpirationMap<>(Duration.standardSeconds(15)); + + /** + * {@link RoomParticipant} by "roomExecutableId:participantId". + */ + private final ExpirationMap roomParticipantCache = + new ExpirationMap<>(); + + /** + * Participant snapshots in {@link MediaData} by "roomExecutableId:participantId". + */ + private final ExpirationMap roomParticipantSnapshotCache = + new ExpirationMap<>(Duration.standardSeconds(15)); + + /** + * @param securityToken + * @param roomExecutableId + * @return {@link Room} for given {@code roomExecutableId} + */ + public Room getRoom(SecurityToken securityToken, String roomExecutableId) + { + synchronized (roomCache) { + Room room = roomCache.get(roomExecutableId); + if (room == null) { + RoomExecutable roomExecutable = getRoomExecutable(securityToken, roomExecutableId); + String resourceId = roomExecutable.getResourceId(); + String resourceRoomId = roomExecutable.getRoomId(); + room = resourceControlService.getRoom(securityToken, resourceId, resourceRoomId); + roomCache.put(roomExecutableId, room); + } + return room; + } + } + + /** + * Modify given {@code room}. + * + * @param securityToken + * @param room + */ + public void modifyRoom(SecurityToken securityToken, String roomExecutableId, Room room) + { + synchronized (roomCache) { + RoomExecutable roomExecutable = getRoomExecutable(securityToken, roomExecutableId); + String resourceId = roomExecutable.getResourceId(); + if (!room.getId().equals(roomExecutable.getRoomId())) { + throw new IllegalArgumentException("Room doesn't correspond to given executable."); + } + resourceControlService.modifyRoom(securityToken, resourceId, room); + roomCache.put(roomExecutableId, room); + } + } + + /** + * @param securityToken + * @param roomExecutableId + * @return collection of {@link RoomParticipant}s for given {@code roomExecutableId} + */ + public List getRoomParticipants(SecurityToken securityToken, String roomExecutableId) + { + synchronized (roomParticipantsCache) { + List roomParticipants = roomParticipantsCache.get(roomExecutableId); + if (roomParticipants == null) { + RoomExecutable roomExecutable = getRoomExecutable(securityToken, roomExecutableId); + String resourceId = roomExecutable.getResourceId(); + String resourceRoomId = roomExecutable.getRoomId(); + roomParticipants = new LinkedList<>(); + synchronized (roomParticipantCache) { + for (RoomParticipant roomParticipant : resourceControlService.listRoomParticipants( + securityToken, resourceId, resourceRoomId)) { + roomParticipants.add(roomParticipant); + roomParticipantCache.put(roomExecutableId + ":" + roomParticipant.getId(), roomParticipant); + } + } + roomParticipantsCache.put(roomExecutableId, roomParticipants); + } + return roomParticipants; + } + } + + /** + * @param securityToken + * @param roomExecutableId + * @param roomParticipantId + * @return {@link RoomParticipant} for given {@code roomExecutableId} and {@code roomParticipantId} + */ + public RoomParticipant getRoomParticipant( + SecurityToken securityToken, + String roomExecutableId, + String roomParticipantId) + { + String cacheId = roomExecutableId + ":" + roomParticipantId; + synchronized (roomParticipantCache) { + RoomParticipant roomParticipant = roomParticipantCache.get(cacheId); + if (roomParticipant == null) { + RoomExecutable roomExecutable = getRoomExecutable(securityToken, roomExecutableId); + String resourceId = roomExecutable.getResourceId(); + String resourceRoomId = roomExecutable.getRoomId(); + roomParticipant = resourceControlService.getRoomParticipant( + securityToken, resourceId, resourceRoomId, roomParticipantId); + if (roomParticipant == null) { + throw new CommonReportSet.ObjectNotExistsException("RoomParticipant", roomParticipantId); + } + roomParticipantCache.put(cacheId, roomParticipant); + } + return roomParticipant; + } + } + + /** + * @param securityToken + * @param roomExecutableId + * @param roomParticipant to be modified in the given {@code roomExecutableId} + */ + public void modifyRoomParticipant( + SecurityToken securityToken, + String roomExecutableId, + RoomParticipant roomParticipant) + { + RoomExecutable roomExecutable = getRoomExecutable(securityToken, roomExecutableId); + String resourceId = roomExecutable.getResourceId(); + String resourceRoomId = roomExecutable.getRoomId(); + roomParticipant.setRoomId(resourceRoomId); + resourceControlService.modifyRoomParticipant(securityToken, resourceId, roomParticipant); + synchronized (roomParticipantCache) { + roomParticipantCache.remove(roomParticipant.getId()); + } + synchronized (roomParticipantsCache) { + roomParticipantsCache.remove(roomExecutableId); + } + } + + /** + * @param securityToken + * @param roomExecutableId + * @param roomParticipants configuration to which all room participants should be modified in the given {@code roomExecutableId} + */ + public void modifyRoomParticipants( + SecurityToken securityToken, + String roomExecutableId, + RoomParticipant roomParticipants) + { + RoomExecutable roomExecutable = getRoomExecutable(securityToken, roomExecutableId); + String resourceId = roomExecutable.getResourceId(); + String resourceRoomId = roomExecutable.getRoomId(); + roomParticipants.setRoomId(resourceRoomId); + resourceControlService.modifyRoomParticipants(securityToken, resourceId, roomParticipants); + synchronized (roomParticipantCache) { + List participants = roomParticipantsCache.get(roomExecutableId); + if (participants != null) { + for (RoomParticipant roomParticipant : participants) { + roomParticipantCache.remove(roomExecutableId + ":" + roomParticipant.getId()); + } + } + } + synchronized (roomParticipantsCache) { + roomParticipantsCache.remove(roomExecutableId); + } + } + + /** + * @param securityToken + * @param roomExecutableId + * @param roomParticipantId + * @return {@link MediaData} snapshot of room participant + */ + public MediaData getRoomParticipantSnapshot( + SecurityToken securityToken, + String roomExecutableId, + String roomParticipantId) + { + String cacheId = roomExecutableId + ":" + roomParticipantId; + synchronized (roomParticipantSnapshotCache) { + if (!roomParticipantSnapshotCache.contains(cacheId)) { + RoomExecutable roomExecutable = getRoomExecutable(securityToken, roomExecutableId); + String resourceId = roomExecutable.getResourceId(); + String resourceRoomId = roomExecutable.getRoomId(); + Set roomParticipantIds = new HashSet(); + roomParticipantIds.add(roomParticipantId); + Map participantSnapshots = resourceControlService.getRoomParticipantSnapshots( + securityToken, resourceId, resourceRoomId, roomParticipantIds); + MediaData roomParticipantSnapshot = participantSnapshots.get(roomParticipantId); + roomParticipantSnapshotCache.put(cacheId, roomParticipantSnapshot); + } + return roomParticipantSnapshotCache.get(cacheId); + } + } + + /** + * @param securityToken + * @param roomExecutableId + * @param roomParticipantId to be disconnected from given {@code roomExecutableId} + */ + public void disconnectRoomParticipant( + SecurityToken securityToken, + String roomExecutableId, + String roomParticipantId) + { + RoomExecutable roomExecutable = getRoomExecutable(securityToken, roomExecutableId); + String resourceId = roomExecutable.getResourceId(); + String resourceRoomId = roomExecutable.getRoomId(); + resourceControlService.disconnectRoomParticipant(securityToken, resourceId, resourceRoomId, roomParticipantId); + synchronized (roomParticipantCache) { + roomParticipantCache.remove(roomParticipantId); + } + synchronized (roomParticipantsCache) { + roomParticipantsCache.remove(roomExecutableId); + } + } + + /** + * @param securityToken + * @param roomExecutableId + * @return {@link RoomExecutable} for given {@code roomExecutableId} + */ + public RoomExecutable getRoomExecutable(SecurityToken securityToken, String roomExecutableId) + { + synchronized (roomExecutableCache) { + RoomExecutable roomExecutable = roomExecutableCache.get(roomExecutableId); + if (roomExecutable == null) { + Executable executable = cache.getExecutable(securityToken, roomExecutableId); + if (executable instanceof RoomExecutable) { + roomExecutable = (RoomExecutable) executable; + } + else { + throw new UnsupportedApiException(executable); + } + roomExecutableCache.put(roomExecutableId, roomExecutable); + } + return roomExecutable; + } + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/ControllerConfig.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/ControllerConfig.java new file mode 100644 index 000000000..98cfac95f --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/ControllerConfig.java @@ -0,0 +1,30 @@ +package cz.cesnet.shongo.controller.rest.config; + +import cz.cesnet.shongo.controller.Controller; +import cz.cesnet.shongo.controller.ControllerClient; +import cz.cesnet.shongo.controller.ControllerConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ControllerConfig +{ + + @Bean + public Controller controller() + { + return Controller.getInstance(); + } + + @Bean + public ControllerConfiguration configuration() + { + return controller().getConfiguration(); + } + + @Bean + public ControllerClient controllerClient() throws Exception + { + return new ControllerClient(configuration().getRpcUrl()); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/OpenApiConfig.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/OpenApiConfig.java new file mode 100644 index 000000000..4dfb6b6d2 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/OpenApiConfig.java @@ -0,0 +1,106 @@ +package cz.cesnet.shongo.controller.rest.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.io.Resource; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.resource.PathResourceResolver; +import org.springframework.web.servlet.resource.ResourceTransformer; +import org.springframework.web.servlet.resource.ResourceTransformerChain; +import org.springframework.web.servlet.resource.TransformedResource; +import org.springframework.web.servlet.resource.WebJarsResourceResolver; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.io.InputStream; + +/** + * Configures OpenApi and SwaggerUI using springdoc. + * + * @author Filip Karnis + */ +@Configuration +@OpenAPIDefinition( + info = @Info(title = "Shongo API", version = "v1"), + security = @SecurityRequirement(name = "bearerAuth") +) +@SecurityScheme( + name = "bearerAuth", + type = SecuritySchemeType.HTTP, + scheme = "bearer" +) +@ComponentScan(basePackages = {"org.springdoc"}) +@Import({ + org.springdoc.core.SpringDocConfiguration.class, + org.springdoc.webmvc.core.SpringDocWebMvcConfiguration.class, + org.springdoc.webmvc.ui.SwaggerConfig.class, + org.springdoc.core.SwaggerUiConfigProperties.class, + org.springdoc.core.SwaggerUiOAuthProperties.class, + org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.class +}) +class OpenApiConfig implements WebMvcConfigurer +{ + + private static final String PET_STORE_URL = "https://petstore.swagger.io/v2/swagger.json"; + private static final String SPRINGDOC_OPENAPI_URL = "/v3/api-docs"; + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) + { + registry.addResourceHandler("/**/*.html") + .addResourceLocations("classpath:/META-INF/resources/webjars/") + .resourceChain(false) + .addResolver(new WebJarsResourceResolver()) + .addResolver(new PathResourceResolver()) + .addTransformer(new IndexPageTransformer()); + } + + /** + * Replaces the default openapi config URL in swagger with the one generated by springdoc. + */ + public static class IndexPageTransformer implements ResourceTransformer + { + @Override + public Resource transform( + HttpServletRequest request, + Resource resource, + ResourceTransformerChain transformerChain) + throws IOException + { + if (resource.getURL().toString().endsWith("/index.html")) { + String html = getHtmlContent(resource); + html = overwritePetStore(html); + return new TransformedResource(resource, html.getBytes()); + } + else { + return resource; + } + } + + private String overwritePetStore(String html) + { + return html.replace(PET_STORE_URL, SPRINGDOC_OPENAPI_URL); + } + + private String getHtmlContent(Resource resource) + { + try { + InputStream inputStream = resource.getInputStream(); + java.util.Scanner s = new java.util.Scanner(inputStream).useDelimiter("\\A"); + String content = s.next(); + inputStream.close(); + return content; + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/ServiceConfig.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/ServiceConfig.java new file mode 100644 index 000000000..2dcf7719f --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/ServiceConfig.java @@ -0,0 +1,49 @@ +package cz.cesnet.shongo.controller.rest.config; + +import cz.cesnet.shongo.controller.ControllerClient; +import cz.cesnet.shongo.controller.api.rpc.AuthorizationService; +import cz.cesnet.shongo.controller.api.rpc.ExecutableService; +import cz.cesnet.shongo.controller.api.rpc.ReservationService; +import cz.cesnet.shongo.controller.api.rpc.ResourceControlService; +import cz.cesnet.shongo.controller.api.rpc.ResourceService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +public class ServiceConfig +{ + + private final ControllerClient controllerClient; + + @Bean + public ReservationService reservationService() + { + return controllerClient.getService(ReservationService.class); + } + + @Bean + public AuthorizationService authorizationService() + { + return controllerClient.getService(AuthorizationService.class); + } + + @Bean + public ExecutableService executableService() + { + return controllerClient.getService(ExecutableService.class); + } + + @Bean + public ResourceService resourceService() + { + return controllerClient.getService(ResourceService.class); + } + + @Bean + public ResourceControlService resourceControlService() + { + return controllerClient.getService(ResourceControlService.class); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/WebMvcConfig.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/WebMvcConfig.java new file mode 100644 index 000000000..4ab57ff59 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/WebMvcConfig.java @@ -0,0 +1,18 @@ +package cz.cesnet.shongo.controller.rest.config; + +import cz.cesnet.shongo.controller.domains.InterDomainControllerLogInterceptor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer +{ + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry + .addInterceptor(new InterDomainControllerLogInterceptor()) + .addPathPatterns("/domain/**"); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/security/AuthFilter.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/security/AuthFilter.java new file mode 100644 index 000000000..5dadb2d63 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/security/AuthFilter.java @@ -0,0 +1,63 @@ +package cz.cesnet.shongo.controller.rest.config.security; + +import cz.cesnet.shongo.controller.ControllerReportSet; +import cz.cesnet.shongo.controller.api.SecurityToken; +import cz.cesnet.shongo.controller.authorization.Authorization; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Authorizes request based on authentication token. + * + * @author Filip Karnis + */ +public class AuthFilter extends GenericFilterBean +{ + + public static final String TOKEN = "TOKEN"; + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String BEARER = "Bearer"; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws ControllerReportSet.SecurityInvalidTokenException, ServletException, IOException + { + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + Authorization authorization = Authorization.getInstance(); + + String accessToken = httpRequest.getHeader(AUTHORIZATION_HEADER); + if (accessToken == null) { + httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "No Authorization header found."); + return; + } + + String[] tokenParts = accessToken.split(BEARER); + if (tokenParts.length != 2) { + httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid access token."); + return; + } + + String sanitizedToken = tokenParts[1].strip(); + SecurityToken securityToken = new SecurityToken(sanitizedToken); + + try { + securityToken.setUserInformation(authorization.getUserInformation(securityToken)); + authorization.validate(securityToken); + } + catch (ControllerReportSet.SecurityInvalidTokenException e) { + httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Request unauthorized."); + return; + } + + httpRequest.setAttribute(TOKEN, securityToken); + chain.doFilter(httpRequest, httpResponse); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/security/SecurityConfig.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/security/SecurityConfig.java new file mode 100644 index 000000000..834ba6b0f --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/security/SecurityConfig.java @@ -0,0 +1,36 @@ +package cz.cesnet.shongo.controller.rest.config.security; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; + +/** + * Configures spring security for REST api server. + * + * @author Filip Karnis + */ +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter +{ + + @Override + public void configure(WebSecurity web) + { + web.ignoring() + .antMatchers("/domain/**") + .antMatchers("/v3/api-docs") + .antMatchers("/swagger-ui/**"); + } + + @Override + protected void configure(HttpSecurity http) throws Exception + { + http.cors().and().csrf().disable(); + AuthFilter authFilter = new AuthFilter(); + http.addFilterAt(authFilter, BasicAuthenticationFilter.class); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/security/WebConfig.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/security/WebConfig.java new file mode 100644 index 000000000..f89010826 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/config/security/WebConfig.java @@ -0,0 +1,32 @@ +package cz.cesnet.shongo.controller.rest.config.security; + +import cz.cesnet.shongo.controller.ControllerConfiguration; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +import java.util.List; + +@Configuration +@EnableWebMvc +@RequiredArgsConstructor +public class WebConfig extends WebMvcConfigurerAdapter { + + private final ControllerConfiguration configuration; + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins(allowedOrigins().toArray(new String[0])) + .allowedMethods("*") + .allowedHeaders("*") + .maxAge(3600); + } + + private List allowedOrigins() + { + return configuration.getRESTApiAllowedOrigins(); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/ParticipantController.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/ParticipantController.java new file mode 100644 index 000000000..71a8599df --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/ParticipantController.java @@ -0,0 +1,188 @@ +package cz.cesnet.shongo.controller.rest.controllers; + +import cz.cesnet.shongo.CommonReportSet; +import cz.cesnet.shongo.ParticipantRole; +import cz.cesnet.shongo.controller.api.AbstractParticipant; +import cz.cesnet.shongo.controller.api.AbstractRoomExecutable; +import cz.cesnet.shongo.controller.api.Executable; +import cz.cesnet.shongo.controller.api.RoomExecutable; +import cz.cesnet.shongo.controller.api.RoomExecutableParticipantConfiguration; +import cz.cesnet.shongo.controller.api.SecurityToken; +import cz.cesnet.shongo.controller.api.UsedRoomExecutable; +import cz.cesnet.shongo.controller.api.request.ListResponse; +import cz.cesnet.shongo.controller.api.rpc.ExecutableService; +import cz.cesnet.shongo.controller.rest.CacheProvider; +import cz.cesnet.shongo.controller.rest.RestApiPath; +import cz.cesnet.shongo.controller.rest.RestCache; +import cz.cesnet.shongo.controller.rest.error.UnsupportedApiException; +import cz.cesnet.shongo.controller.rest.models.participant.ParticipantConfigurationModel; +import cz.cesnet.shongo.controller.rest.models.participant.ParticipantModel; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import static cz.cesnet.shongo.controller.rest.config.security.AuthFilter.TOKEN; + +/** + * Rest controller for participant endpoints. + * + * @author Filip Karnis + */ +@RestController +@RequestMapping(RestApiPath.PARTICIPANTS) +@RequiredArgsConstructor +public class ParticipantController +{ + + private final RestCache cache; + private final ExecutableService executableService; + + @Operation(summary = "Lists reservation request participants.") + @GetMapping + ListResponse listRequestParticipants( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id, + @RequestParam(value = "start", required = false) Integer start, + @RequestParam(value = "count", required = false) Integer count) + { + final CacheProvider cacheProvider = new CacheProvider(cache, securityToken); + + // Get room executable + String executableId = cache.getExecutableId(securityToken, id); + AbstractRoomExecutable roomExecutable = getRoomExecutable(securityToken, executableId); + + List participants = new LinkedList<>(); + + // Add reused room participants as read-only + if (roomExecutable instanceof UsedRoomExecutable) { + UsedRoomExecutable usedRoomExecutable = (UsedRoomExecutable) roomExecutable; + String reusedRoomExecutableId = usedRoomExecutable.getReusedRoomExecutableId(); + RoomExecutable reusedRoomExecutable = + (RoomExecutable) getRoomExecutable(securityToken, reusedRoomExecutableId); + List reusedRoomParticipants = + reusedRoomExecutable.getParticipantConfiguration().getParticipants(); + reusedRoomParticipants.sort(Comparator.comparing(o -> Integer.valueOf(o.getId()))); + reusedRoomParticipants.forEach(participant -> { + participant.setId((String) null); + participants.add(participant); + }); + } + + // Add room participants + List roomParticipants = roomExecutable.getParticipantConfiguration().getParticipants(); + roomParticipants.sort(Comparator.comparing(o -> Integer.valueOf(o.getId()))); + participants.addAll(roomParticipants); + + List items = participants.stream().map( + participant -> new ParticipantModel(participant, cacheProvider) + ).collect(Collectors.toList()); + return ListResponse.fromRequest(start, count, items); + } + + @ResponseStatus(HttpStatus.CREATED) + @Operation(summary = "Adds new participant to reservation request.") + @PostMapping + void addParticipant( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id, + @RequestBody ParticipantModel newParticipant) + { + String executableId = cache.getExecutableId(securityToken, id); + AbstractRoomExecutable roomExecutable = getRoomExecutable(securityToken, executableId); + + CacheProvider cacheProvider = new CacheProvider(cache, securityToken); + RoomExecutableParticipantConfiguration participantConfiguration = roomExecutable.getParticipantConfiguration(); + + // Initialize model from API + ParticipantConfigurationModel participantConfigurationModel = new ParticipantConfigurationModel(); + for (AbstractParticipant existingParticipant : participantConfiguration.getParticipants()) { + participantConfigurationModel.addParticipant(new ParticipantModel(existingParticipant, cacheProvider)); + } + // Modify model + participantConfigurationModel.addParticipant(newParticipant); + // Initialize API from model + participantConfiguration.clearParticipants(); + for (ParticipantModel participantModel : participantConfigurationModel.getParticipants()) { + participantConfiguration.addParticipant(participantModel.toApi()); + } + executableService.modifyExecutableConfiguration(securityToken, executableId, participantConfiguration); + cache.clearExecutable(executableId); + } + + @Operation(summary = "Adds new participant to reservation request.") + @PutMapping(RestApiPath.PARTICIPANTS_ID_SUFFIX) + void updateParticipant( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id, + @PathVariable String participantId, + @RequestParam ParticipantRole role) + { + String executableId = cache.getExecutableId(securityToken, id); + AbstractRoomExecutable roomExecutable = getRoomExecutable(securityToken, executableId); + + RoomExecutableParticipantConfiguration participantConfiguration = roomExecutable.getParticipantConfiguration(); + + // Modify model + ParticipantModel oldParticipant = getParticipant(participantConfiguration, participantId, securityToken); + oldParticipant.setRole(role); + // Initialize API from model + participantConfiguration.removeParticipantById(participantId); + participantConfiguration.addParticipant(oldParticipant.toApi()); + + executableService.modifyExecutableConfiguration(securityToken, executableId, participantConfiguration); + cache.clearExecutable(executableId); + } + + @Operation(summary = "Removes participant from reservation request.") + @DeleteMapping(RestApiPath.PARTICIPANTS_ID_SUFFIX) + void removeParticipant( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id, + @PathVariable String participantId) + { + String executableId = cache.getExecutableId(securityToken, id); + AbstractRoomExecutable roomExecutable = getRoomExecutable(securityToken, executableId); + RoomExecutableParticipantConfiguration participantConfiguration = roomExecutable.getParticipantConfiguration(); + ParticipantModel oldParticipant = getParticipant(participantConfiguration, participantId, securityToken); + participantConfiguration.removeParticipantById(oldParticipant.getId()); + executableService.modifyExecutableConfiguration(securityToken, executableId, participantConfiguration); + cache.clearExecutable(executableId); + } + + private AbstractRoomExecutable getRoomExecutable(SecurityToken securityToken, String executableId) + { + Executable executable = cache.getExecutable(securityToken, executableId); + if (executable instanceof AbstractRoomExecutable) { + return (AbstractRoomExecutable) executable; + } + else { + throw new UnsupportedApiException(executable); + } + } + + protected ParticipantModel getParticipant(RoomExecutableParticipantConfiguration participantConfiguration, + String participantId, SecurityToken securityToken) + { + AbstractParticipant participant = participantConfiguration.getParticipant(participantId); + if (participant == null) { + throw new CommonReportSet.ObjectNotExistsException("Participant", participantId); + } + return new ParticipantModel(participant, new CacheProvider(cache, securityToken)); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/RecordingController.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/RecordingController.java new file mode 100644 index 000000000..9a880415c --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/RecordingController.java @@ -0,0 +1,91 @@ +package cz.cesnet.shongo.controller.rest.controllers; + +import cz.cesnet.shongo.controller.api.RecordingService; +import cz.cesnet.shongo.controller.api.ResourceRecording; +import cz.cesnet.shongo.controller.api.SecurityToken; +import cz.cesnet.shongo.controller.api.request.ExecutableRecordingListRequest; +import cz.cesnet.shongo.controller.api.request.ExecutableServiceListRequest; +import cz.cesnet.shongo.controller.api.request.ListResponse; +import cz.cesnet.shongo.controller.api.rpc.ExecutableService; +import cz.cesnet.shongo.controller.api.rpc.ResourceControlService; +import cz.cesnet.shongo.controller.rest.RestApiPath; +import cz.cesnet.shongo.controller.rest.RestCache; +import cz.cesnet.shongo.controller.rest.models.recording.RecordingModel; +import cz.cesnet.shongo.controller.scheduler.SchedulerReportSet; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.stream.Collectors; + +import static cz.cesnet.shongo.controller.rest.config.security.AuthFilter.TOKEN; + +/** + * Rest controller for recording endpoints. + * + * @author Filip Karnis + */ +@RestController +@RequestMapping(RestApiPath.RECORDINGS) +@RequiredArgsConstructor +public class RecordingController +{ + + private final RestCache cache; + private final ExecutableService executableService; + private final ResourceControlService resourceControlService; + + @Operation(summary = "Lists reservation request recordings.") + @GetMapping + ListResponse listRequestRecordings( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id, + @RequestParam(value = "start", required = false) Integer start, + @RequestParam(value = "count", required = false) Integer count, + @RequestParam(value = "sort", required = false, defaultValue = "START") + ExecutableRecordingListRequest.Sort sort, + @RequestParam(value = "sort-desc", required = false, defaultValue = "true") boolean sortDescending) + throws SchedulerReportSet.RoomExecutableNotExistsException + { + String executableId = cache.getExecutableId(securityToken, id); + if (executableId == null) { + throw new SchedulerReportSet.RoomExecutableNotExistsException(); + } + ExecutableRecordingListRequest request = new ExecutableRecordingListRequest(); + request.setSecurityToken(securityToken); + request.setExecutableId(executableId); + request.setStart(start); + request.setCount(count); + request.setSort(sort); + request.setSortDescending(sortDescending); + + ListResponse response = executableService.listExecutableRecordings(request); + + List items = response.getItems().stream().map(RecordingModel::new).collect(Collectors.toList()); + return ListResponse.fromRequest(response.getStart(), response.getCount(), items); + } + + @Operation(summary = "Deletes recording from reservation request.") + @DeleteMapping(RestApiPath.RECORDINGS_ID_SUFFIX) + void deleteRequestRecording( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id, + @PathVariable String recordingId) + { + String executableId = cache.getExecutableId(securityToken, id); + ExecutableServiceListRequest request = new ExecutableServiceListRequest(securityToken, executableId, RecordingService.class); + List executableServices = executableService.listExecutableServices(request).getItems(); + if (executableServices.isEmpty()) { + throw new IllegalArgumentException("No recording service found for executable " + executableId); + } + String resId = ((RecordingService) executableServices.get(0)).getResourceId(); + resourceControlService.deleteRecording(securityToken, resId, recordingId); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/ReportController.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/ReportController.java new file mode 100644 index 000000000..5a62faafc --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/ReportController.java @@ -0,0 +1,47 @@ +package cz.cesnet.shongo.controller.rest.controllers; + +import cz.cesnet.shongo.controller.rest.ErrorHandler; +import cz.cesnet.shongo.controller.rest.RestApiPath; +import cz.cesnet.shongo.controller.rest.models.report.ReportModel; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.mail.MessagingException; + +/** + * Rest controller for report endpoints. + * + * @author Filip Karnis + */ +@Slf4j +@SecurityRequirements +@RestController +@RequestMapping(RestApiPath.REPORT) +@RequiredArgsConstructor +public class ReportController +{ + + private final ErrorHandler errorHandler; + + /** + * Handle problem report. + */ + @Operation(summary = "Report a problem to administrators.") + @PostMapping + public void reportProblem( + @RequestBody ReportModel reportModel) throws MessagingException + { + String emailReplyTo = reportModel.getEmail(); + String emailSubject = reportModel.getEmailSubject(); + String emailContent = reportModel.getEmailContent(); + + log.info("Sending problem report: {}", reportModel); + errorHandler.sendEmailToAdministrator(emailReplyTo, emailSubject, emailContent); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/ReservationDeviceController.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/ReservationDeviceController.java new file mode 100644 index 000000000..db6009aca --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/ReservationDeviceController.java @@ -0,0 +1,48 @@ +package cz.cesnet.shongo.controller.rest.controllers; + +import cz.cesnet.shongo.controller.api.ReservationDevice; +import cz.cesnet.shongo.controller.api.SecurityToken; +import cz.cesnet.shongo.controller.api.rpc.AuthorizationService; +import cz.cesnet.shongo.controller.rest.RestApiPath; +import cz.cesnet.shongo.controller.rest.models.reservationdevice.ReservationDeviceModel; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static cz.cesnet.shongo.controller.rest.config.security.AuthFilter.TOKEN; + +/** + * Rest controller for endpoints related to reservation devices. + * + * @author Michal Drobňák + */ +@RestController +@RequestMapping(RestApiPath.RESERVATION_DEVICE) +@RequiredArgsConstructor +@Slf4j +public class ReservationDeviceController { + + private final AuthorizationService authorizationService; + + @Operation(summary = "Get reservation device associated with Bearer token.") + @GetMapping + ResponseEntity getReservationDevice( + @RequestAttribute(TOKEN) SecurityToken securityToken + ) { + ReservationDevice device = authorizationService.getReservationDevice(securityToken); + + if (device == null) { + log.info("Device not found"); + return ResponseEntity.notFound().build(); + } + + ReservationDeviceModel model = new ReservationDeviceModel(device); + log.info("Get reservation device: {}", model); + return ResponseEntity.ok().body(model); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/ReservationRequestController.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/ReservationRequestController.java new file mode 100644 index 000000000..cb2f0cde6 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/ReservationRequestController.java @@ -0,0 +1,505 @@ +package cz.cesnet.shongo.controller.rest.controllers; + +import cz.cesnet.shongo.Temporal; +import cz.cesnet.shongo.api.UserInformation; +import cz.cesnet.shongo.controller.ObjectPermission; +import cz.cesnet.shongo.controller.ObjectRole; +import cz.cesnet.shongo.controller.api.AbstractReservationRequest; +import cz.cesnet.shongo.controller.api.AbstractRoomExecutable; +import cz.cesnet.shongo.controller.api.AllocationState; +import cz.cesnet.shongo.controller.api.RecordingService; +import cz.cesnet.shongo.controller.api.ReservationRequestSummary; +import cz.cesnet.shongo.controller.api.ResourceSummary; +import cz.cesnet.shongo.controller.api.SecurityToken; +import cz.cesnet.shongo.controller.api.request.ExecutableServiceListRequest; +import cz.cesnet.shongo.controller.api.request.ListResponse; +import cz.cesnet.shongo.controller.api.request.ReservationRequestListRequest; +import cz.cesnet.shongo.controller.api.TagData; +import cz.cesnet.shongo.controller.api.rpc.AuthorizationService; +import cz.cesnet.shongo.controller.api.rpc.ExecutableService; +import cz.cesnet.shongo.controller.api.rpc.ReservationService; +import cz.cesnet.shongo.controller.api.AuxDataFilter; +import cz.cesnet.shongo.controller.rest.CacheProvider; +import cz.cesnet.shongo.controller.rest.RestApiPath; +import cz.cesnet.shongo.controller.rest.RestCache; +import cz.cesnet.shongo.controller.rest.models.IdModel; +import cz.cesnet.shongo.controller.rest.models.TechnologyModel; +import cz.cesnet.shongo.controller.rest.models.reservationrequest.ReservationRequestCreateModel; +import cz.cesnet.shongo.controller.rest.models.reservationrequest.ReservationRequestDetailModel; +import cz.cesnet.shongo.controller.rest.models.reservationrequest.ReservationRequestModel; +import cz.cesnet.shongo.controller.rest.models.reservationrequest.SpecificationType; +import cz.cesnet.shongo.controller.rest.models.roles.UserRoleModel; +import cz.cesnet.shongo.controller.rest.models.room.RoomAuthorizedData; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.springdoc.api.annotations.ParameterObject; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static cz.cesnet.shongo.controller.rest.config.security.AuthFilter.TOKEN; + +/** + * Rest controller for reservation request endpoints. + * + * @author Filip Karnis + */ +@RestController +@RequestMapping(RestApiPath.RESERVATION_REQUESTS) +@RequiredArgsConstructor +public class ReservationRequestController +{ + + private final RestCache cache; + private final ReservationService reservationService; + private final AuthorizationService authorizationService; + private final ExecutableService executableService; + + @Operation(summary = "Lists reservation requests.") + @GetMapping + ListResponse listRequests( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @RequestParam(value = "start", required = false) Integer start, + @RequestParam(value = "count", required = false) Integer count, + @RequestParam(value = "sort", required = false, + defaultValue = "DATETIME") ReservationRequestListRequest.Sort sort, + @RequestParam(value = "sort_desc", required = false, defaultValue = "true") boolean sortDescending, + @RequestParam(value = "allocation_state", required = false) AllocationState allocationState, + @RequestParam(value = "parentRequestId", required = false) String permanentRoomId, + @RequestParam(value = "technology", required = false) TechnologyModel technology, + @RequestParam(value = "interval_from", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + DateTime intervalFrom, + @RequestParam(value = "interval_to", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + DateTime intervalTo, + @RequestParam(value = "user_id", required = false) String userId, + @RequestParam(value = "participant_user_id", required = false) String participantUserId, + @RequestParam(value = "search", required = false) String search, + @RequestParam(value = "type", required = false) Set reservationTypes, + @RequestParam(value = "resource", required = false) Set resourceId) + { + if (reservationTypes == null) { + reservationTypes = new HashSet<>(); + } + if (resourceId == null) { + resourceId = new HashSet<>(); + } + + // room capacity does not have resource_id + // filter VIRTUAL_ROOMS by resource_id and then call recursively with parentRequestId + if (reservationTypes.contains(ReservationType.ROOM_CAPACITY) && !resourceId.isEmpty()) { + Set virtualRoomReservationTypes = new HashSet<>(List.of(ReservationType.VIRTUAL_ROOM)); + ListResponse virtualRooms = listRequests(securityToken, start, count, sort, + sortDescending, allocationState, permanentRoomId, technology, intervalFrom, intervalTo, userId, + participantUserId, search, virtualRoomReservationTypes, resourceId); + + DateTime finalIntervalFrom = intervalFrom; + DateTime finalIntervalTo = intervalTo; + Set capacityReservationTypes = new HashSet<>(List.of(ReservationType.ROOM_CAPACITY)); + List response = virtualRooms.getItems() + .stream() + .map(room -> listRequests(securityToken, start, count, sort, sortDescending, allocationState, + room.getId(), technology, finalIntervalFrom, finalIntervalTo, userId, participantUserId, + search, capacityReservationTypes, null) + .getItems() + .stream() + .peek(capacities -> capacities.getVirtualRoomData() + .setTechnology(room.getVirtualRoomData().getTechnology()) + ) + .collect(Collectors.toList()) + ) + .flatMap(List::stream) + .collect(Collectors.toList()); + + if (reservationTypes.size() > 1) { + reservationTypes.remove(ReservationType.ROOM_CAPACITY); + List other = listRequests( + securityToken, start, count, sort, sortDescending, allocationState, permanentRoomId, technology, + intervalFrom, intervalTo, userId, participantUserId, search, reservationTypes, resourceId + ).getItems(); + response.addAll(other); + } + + return ListResponse.fromRequest(start, count, response); + } + + ReservationRequestListRequest request = new ReservationRequestListRequest(); + + request.setSecurityToken(securityToken); + request.setStart(start); + request.setCount(count); + request.setSort(sort); + request.setSortDescending(sortDescending); + request.setAllocationState(allocationState); + request.setParticipantUserId(participantUserId); + request.setSearch(search); + request.setSpecificationResourceIds(resourceId); + + if (permanentRoomId != null) { + if (reservationTypes.contains(ReservationType.PHYSICAL_RESOURCE)) { + request.setParentReservationRequestId(permanentRoomId); + } else { + request.setReusedReservationRequestId(permanentRoomId); + reservationTypes.add(ReservationType.ROOM_CAPACITY); + } + } + + if (reservationTypes.contains(ReservationType.VIRTUAL_ROOM)) { + request.addSpecificationType(ReservationRequestSummary.SpecificationType.ROOM); + request.addSpecificationType(ReservationRequestSummary.SpecificationType.PERMANENT_ROOM); + } + if (reservationTypes.contains(ReservationType.ROOM_CAPACITY)) { + request.addSpecificationType(ReservationRequestSummary.SpecificationType.USED_ROOM); + } + if (reservationTypes.contains(ReservationType.PHYSICAL_RESOURCE)) { + request.addSpecificationType(ReservationRequestSummary.SpecificationType.RESOURCE); + } + + if (technology != null) { + request.setSpecificationTechnologies(technology.getTechnologies()); + } + + if (intervalFrom != null || intervalTo != null) { + if (intervalFrom == null) { + intervalFrom = Temporal.DATETIME_INFINITY_START; + } + if (intervalTo == null) { + intervalTo = Temporal.DATETIME_INFINITY_END; + } + if (intervalTo.isAfter(intervalFrom)) { + request.setInterval(new Interval(intervalFrom, intervalTo)); + } + } + if (userId != null && UserInformation.isLocal(userId)) { + request.setUserId(userId); + } + + ListResponse response = reservationService.listReservationRequests(request); + Map> permissionsByReservationRequestId = + cache.getReservationRequestsPermissions(securityToken, response.getItems()); + + ListResponse listResponse = new ListResponse<>(); + listResponse.addAll(response.getItems().stream().map(item -> { + UserInformation user = cache.getUserInformation(securityToken, item.getUserId()); + String resource = item.getResourceId(); + ResourceSummary resourceSummary = null; + if (resource != null) { + resourceSummary = cache.getResourceSummary(securityToken, resource); + } + return new ReservationRequestModel( + item, item, permissionsByReservationRequestId, user, resourceSummary + ); + }).collect(Collectors.toList())); + listResponse.setStart(response.getStart()); + listResponse.setCount(response.getCount()); + return listResponse; + } + + @ResponseStatus(HttpStatus.CREATED) + @Operation(summary = "Creates reservation request.") + @PostMapping + IdModel createRequest( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @RequestBody ReservationRequestCreateModel request) + { + CacheProvider cacheProvider = new CacheProvider(cache, securityToken); + request.setCacheProvider(cacheProvider); + + String resourceId = request.getResourceId(); + if (resourceId != null) { + ResourceSummary resourceSummary = cacheProvider.getResourceSummary(resourceId); + request.setTechnology(TechnologyModel.find(resourceSummary.getTechnologies())); + } + UserInformation userInformation = securityToken.getUserInformation(); + + if (request.getSpecificationType() == SpecificationType.VIRTUAL_ROOM) { + // Add default participant + request.addRoomParticipant(userInformation, request.getDefaultOwnerParticipantRole()); + + // Create VIRTUAL_ROOM + String reservationId = reservationService.createReservationRequest(securityToken, request.toApi()); + + // Add default role + request.setId(reservationId); + UserRoleModel userRoleModel = request.addUserRole(userInformation, ObjectRole.OWNER); + authorizationService.createAclEntry(securityToken, userRoleModel.toApi()); + + // Set request to ROOM_CAPACITY for created VIRTUAL_ROOM + request.setSpecificationType(SpecificationType.ROOM_CAPACITY); + request.setRoomReservationRequestId(reservationId); + request.clearRoomParticipants(); + } + + String reservationId = reservationService.createReservationRequest(securityToken, request.toApi()); + request.setId(reservationId); + + // Create default role for the user + if (request.getSpecificationType() != SpecificationType.ROOM_CAPACITY) { + UserRoleModel userRoleModel = request.addUserRole(userInformation, ObjectRole.OWNER); + authorizationService.createAclEntry(securityToken, userRoleModel.toApi()); + } + return new IdModel(reservationId); + } + + @Operation(summary = "Returns reservation request.") + @GetMapping(RestApiPath.ID_SUFFIX) + ReservationRequestDetailModel getRequest( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id) + { + CacheProvider cacheProvider = new CacheProvider(cache, securityToken); + ReservationRequestSummary summary = cache.getReservationRequestSummary(securityToken, id); + + String roomId = cache.getExecutableId(securityToken, id); + RoomAuthorizedData authorizedData = null; + boolean isRecordingActive = false; + if (roomId != null) { + AbstractRoomExecutable roomExecutable = + (AbstractRoomExecutable) executableService.getExecutable(securityToken, roomId); + authorizedData = new RoomAuthorizedData(roomExecutable); + + ExecutableServiceListRequest request = new ExecutableServiceListRequest(securityToken, roomId, RecordingService.class); + List executableServices = executableService.listExecutableServices(request).getItems(); + if (!executableServices.isEmpty()) { + isRecordingActive = executableServices.get(0).isActive(); + } + } + + List requests = new ArrayList<>(); + requests.add(summary); + Map> permissionsByReservationRequestId = + cache.getReservationRequestsPermissions(securityToken, requests); + + UserInformation ownerInformation = cache.getUserInformation(securityToken, summary.getUserId()); + + String resourceId = summary.getResourceId(); + ResourceSummary resourceSummary = null; + if (resourceId != null) { + resourceSummary = cacheProvider.getResourceSummary(resourceId); + } + ReservationRequestSummary virtualRoomSummary = summary; + + List historySummaries = reservationService.getReservationRequestHistory( + securityToken, id + ); + Map> permissionsByReservationHistory = + cache.getReservationRequestsPermissions(securityToken, historySummaries); + List history = + historySummaries + .stream() + .map(item -> { + UserInformation user = cache.getUserInformation(securityToken, item.getUserId()); + String resource = item.getResourceId(); + ResourceSummary resourceSum = null; + if (resource != null) { + resourceSum = cacheProvider.getResourceSummary(resource); + } + return new ReservationRequestModel( + item, + item, + permissionsByReservationHistory, + user, + resourceSum + ); + }) + .collect(Collectors.toList()); + + // If the request is a ROOM_CAPACITY, then get virtual room data from the room + String virtualRoomId = summary.getReusedReservationRequestId(); + if (virtualRoomId != null) { + virtualRoomSummary = cache.getReservationRequestSummary( + securityToken, virtualRoomId + ); + } + + AbstractReservationRequest abstractReservationRequest = reservationService.getReservationRequest(securityToken, id); + + ReservationRequestDetailModel detailModel = new ReservationRequestDetailModel( + summary, virtualRoomSummary, permissionsByReservationRequestId, ownerInformation, + abstractReservationRequest, authorizedData, history, resourceSummary + ); + detailModel.getRoomCapacityData().setIsRecordingActive(isRecordingActive); + return detailModel; + } + + @Operation(summary = "Modifies reservation request.") + @PatchMapping(RestApiPath.ID_SUFFIX) + IdModel modifyRequest( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id, + @RequestBody ReservationRequestCreateModel request) + { + CacheProvider cacheProvider = new CacheProvider(cache, securityToken); + + AbstractReservationRequest originalRequest = reservationService.getReservationRequest(securityToken, id); + ReservationRequestCreateModel modifiedRequest = + new ReservationRequestCreateModel(originalRequest, cacheProvider); + + if (request.getRoomName() != null) { + modifiedRequest.setRoomName(request.getRoomName()); + } + if (request.getDescription() != null) { + modifiedRequest.setDescription(request.getDescription()); + } + if (request.getSlot() != null) { + modifiedRequest.setSlot(request.getSlot()); + } + if (request.getPeriodicity() != null) { + modifiedRequest.setPeriodicity(request.getPeriodicity()); + } + if (request.getResourceId() != null) { + ResourceSummary resourceSummary = cacheProvider.getResourceSummary(request.getResourceId()); + modifiedRequest.setTechnology(TechnologyModel.find(resourceSummary.getTechnologies())); + } + if (request.getAdminPin() != null) { + modifiedRequest.setAdminPin(request.getAdminPin()); + } + if (request.getGuestPin() != null) { + modifiedRequest.setGuestPin(request.getGuestPin()); + } + if (request.getParticipantCount() != null) { + modifiedRequest.setParticipantCount(request.getParticipantCount()); + } + if (request.getAuxData() != null) { + modifiedRequest.setAuxData(request.getAuxData()); + } + modifiedRequest.setAllowGuests(request.isAllowGuests()); + modifiedRequest.setRecord(request.isRecord()); + + String newId = reservationService.modifyReservationRequest(securityToken, modifiedRequest.toApi()); + return new IdModel(newId); + } + + @Operation(summary = "Deletes reservation request.") + @DeleteMapping(RestApiPath.ID_SUFFIX) + void deleteRequest( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id) + { + reservationService.deleteReservationRequest(securityToken, id); + } + + @Operation(summary = "Deletes multiple reservation requests.") + @DeleteMapping + void deleteRequests( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @RequestBody List ids) + { + ids.forEach(id -> reservationService.deleteReservationRequest(securityToken, id)); + } + + @Operation(summary = "Lists reservation requests waiting for confirmation of resources owned by a user.") + @GetMapping(RestApiPath.RESERVATION_REQUESTS_AWAITING_CONFIRMATION) + public ListResponse listOwnedReservationRequests( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @RequestParam(value = "interval_from", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + DateTime intervalFrom, + @RequestParam(value = "interval_to", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + DateTime intervalTo, + @RequestParam(value = "resource", required = false) Set resourceId, + @RequestParam(value = "start", required = false) Integer start, + @RequestParam(value = "count", required = false) Integer count, + @RequestParam(value = "sort", required = false, defaultValue = "SLOT_START") ReservationRequestListRequest.Sort sort, + @RequestParam(value = "sort_desc", required = false, defaultValue = "true") boolean sortDescending + ) { + if (resourceId == null) { + resourceId = new HashSet<>(); + } + + ReservationRequestListRequest requestListRequest = new ReservationRequestListRequest(securityToken); + requestListRequest.setStart(start); + requestListRequest.setCount(count); + requestListRequest.setSort(sort); + requestListRequest.setSortDescending(sortDescending); + requestListRequest.setAllocationState(AllocationState.CONFIRM_AWAITING); + if (intervalFrom == null) { + intervalFrom = Temporal.DATETIME_INFINITY_START; + } + if (intervalTo == null) { + intervalTo = Temporal.DATETIME_INFINITY_END; + } + Interval interval = new Interval(intervalFrom, intervalTo); + requestListRequest.setInterval(interval); + requestListRequest.setSpecificationResourceIds(resourceId); + + ListResponse response = reservationService.listOwnedResourcesReservationRequests(requestListRequest); + + ListResponse listResponse = new ListResponse<>(); + listResponse.addAll(response.getItems().stream().map(item -> { + String resource = item.getResourceId(); + ResourceSummary resourceSummary = null; + if (resource != null) { + resourceSummary = cache.getResourceSummary(securityToken, resource); + } + return new ReservationRequestModel( + item, item, resourceSummary + ); + }).collect(Collectors.toList())); + listResponse.setStart(response.getStart()); + listResponse.setCount(response.getCount()); + return listResponse; + } + + @Operation(summary = "Accepts reservation request.") + @PostMapping(RestApiPath.RESERVATION_REQUESTS_ACCEPT) + void acceptRequest( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id) + { + reservationService.confirmReservationRequest(securityToken, id, true); + } + + @Operation(summary = "Rejects reservation request.") + @PostMapping(RestApiPath.RESERVATION_REQUESTS_REJECT) + void rejectRequest( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id, + @RequestParam(required = false) String reason) + { + reservationService.denyReservationRequest(securityToken, id, reason); + } + + @Operation(summary = "Reverts reservation request modifications.") + @PostMapping(RestApiPath.RESERVATION_REQUESTS_REVERT) + void revertRequest( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id) + { + reservationService.revertReservationRequest(securityToken, id); + } + + @GetMapping(RestApiPath.RESERVATION_REQUESTS_AUX_DATA) + List> getTagData( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id, + @ParameterObject AuxDataFilter filter) + { + return reservationService.getReservationRequestTagData(securityToken, id, filter); + } + + public enum ReservationType + { + VIRTUAL_ROOM, + PHYSICAL_RESOURCE, + ROOM_CAPACITY + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/ResourceController.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/ResourceController.java new file mode 100644 index 000000000..f5d5bcd13 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/ResourceController.java @@ -0,0 +1,141 @@ +package cz.cesnet.shongo.controller.rest.controllers; + +import cz.cesnet.shongo.controller.ObjectPermission; +import cz.cesnet.shongo.controller.api.Resource; +import cz.cesnet.shongo.controller.api.ResourceSummary; +import cz.cesnet.shongo.controller.api.SecurityToken; +import cz.cesnet.shongo.controller.api.request.ListResponse; +import cz.cesnet.shongo.controller.api.request.ResourceListRequest; +import cz.cesnet.shongo.controller.api.rpc.ResourceService; +import cz.cesnet.shongo.controller.rest.RestApiPath; +import cz.cesnet.shongo.controller.rest.RestCache; +import cz.cesnet.shongo.controller.rest.models.TechnologyModel; +import cz.cesnet.shongo.controller.rest.models.resource.ReservationModel; +import cz.cesnet.shongo.controller.rest.models.resource.ResourceCapacity; +import cz.cesnet.shongo.controller.rest.models.resource.ResourceCapacityUtilization; +import cz.cesnet.shongo.controller.rest.models.resource.ResourceModel; +import cz.cesnet.shongo.controller.rest.models.resource.ResourceUtilizationDetailModel; +import cz.cesnet.shongo.controller.rest.models.resource.ResourceUtilizationModel; +import cz.cesnet.shongo.controller.rest.models.resource.ResourcesUtilization; +import cz.cesnet.shongo.controller.rest.models.resource.Unit; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static cz.cesnet.shongo.controller.rest.config.security.AuthFilter.TOKEN; + +/** + * Rest controller for resource endpoints. + * + * @author Filip Karnis + */ +@Slf4j +@RestController +@RequestMapping(RestApiPath.RESOURCES) +@RequiredArgsConstructor +public class ResourceController +{ + + private final RestCache cache; + private final ResourceService resourceService; + + /** + * Lists {@link Resource}s. + */ + @Operation(summary = "Lists available resources.") + @GetMapping + List listResources( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @RequestParam(value = "technology", required = false) TechnologyModel technology, + @RequestParam(value = "tag", required = false) String tag) + { + ResourceListRequest resourceListRequest = new ResourceListRequest(securityToken); + resourceListRequest.setAllocatable(true); + if (technology != null) { + resourceListRequest.setTechnologies(technology.getTechnologies()); + } + if (tag != null) { + resourceListRequest.setTagName(tag); + } + + // Filter only reservable resources + resourceListRequest.setPermission(ObjectPermission.RESERVE_RESOURCE); + + ListResponse accessibleResources = resourceService.listResources(resourceListRequest); + + return accessibleResources.getItems() + .stream() + .map(ResourceModel::new) + // Filter only resources with either technology or tag + .filter(resource -> !(resource.getTechnology() == null && resource.getTags().isEmpty())) + .collect(Collectors.toList()); + } + + /** + * Gets {@link ResourceCapacityUtilization}s. + */ + @Operation(summary = "Returns resource utilization.") + @GetMapping(RestApiPath.CAPACITY_UTILIZATION) + ListResponse listResourcesUtilization( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @RequestParam(value = "interval_from") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + DateTime intervalFrom, + @RequestParam(value = "interval_to") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + DateTime intervalTo, + @RequestParam Unit unit, + @RequestParam(required = false) int start, + @RequestParam(required = false) int count, + @RequestParam(required = false) boolean refresh) + { + Interval interval = new Interval(intervalFrom, intervalTo); + Period period = unit.getPeriod(); + ResourcesUtilization resourcesUtilization = cache.getResourcesUtilization(securityToken, refresh); + var utilization = resourcesUtilization.getUtilization(interval, period); + List items = new ArrayList<>(); + utilization.forEach((utilizationInterval, resourceCapacityUtilization) -> + items.add(ResourceUtilizationModel.fromApi(utilizationInterval, resourceCapacityUtilization)) + ); + return ListResponse.fromRequest(start, count, items); + } + + /** + * Lists {@link Resource}s. + */ + @Operation(summary = "Gets resource utilization.") + @GetMapping(RestApiPath.CAPACITY_UTILIZATION_DETAIL) + ResourceUtilizationDetailModel getResourceUtilization( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable("id") String resourceId, + @RequestParam(value = "interval_from") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) DateTime intervalFrom, + @RequestParam(value = "interval_to") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) DateTime intervalTo) + { + Class resourceCapacityClass = ResourceCapacity.Room.class; + ResourcesUtilization resourcesUtilization = cache.getResourcesUtilization(securityToken, false); + ResourceCapacity resourceCapacity = resourcesUtilization.getResourceCapacity(resourceId, resourceCapacityClass); + ResourceCapacityUtilization resourceCapacityUtilization = + resourcesUtilization.getUtilization(resourceCapacity, new Interval(intervalFrom, intervalTo)); + + ResourceCapacity.Room roomCapacity = (ResourceCapacity.Room) resourceCapacity; + List reservations = (resourceCapacityUtilization != null) + ? resourceCapacityUtilization.getReservations().stream().map(res -> + ReservationModel.fromApi(res, cache.getUserInformation(securityToken, res.getUserId())) + ).collect(Collectors.toList()) + : Collections.emptyList(); + return ResourceUtilizationDetailModel.fromApi(resourceCapacityUtilization, roomCapacity, reservations); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/RoomController.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/RoomController.java new file mode 100644 index 000000000..a3c0e7886 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/RoomController.java @@ -0,0 +1,90 @@ +package cz.cesnet.shongo.controller.rest.controllers; + +import cz.cesnet.shongo.controller.api.AbstractRoomExecutable; +import cz.cesnet.shongo.controller.api.ExecutableSummary; +import cz.cesnet.shongo.controller.api.SecurityToken; +import cz.cesnet.shongo.controller.api.request.ExecutableListRequest; +import cz.cesnet.shongo.controller.api.request.ListResponse; +import cz.cesnet.shongo.controller.api.rpc.ExecutableService; +import cz.cesnet.shongo.controller.rest.RestApiPath; +import cz.cesnet.shongo.controller.rest.RestCache; +import cz.cesnet.shongo.controller.rest.models.room.RoomAuthorizedData; +import cz.cesnet.shongo.controller.rest.models.room.RoomModel; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.stream.Collectors; + +import static cz.cesnet.shongo.controller.rest.config.security.AuthFilter.TOKEN; + +/** + * Rest controller for room endpoints. + * + * @author Filip Karnis + */ +@RestController +@RequestMapping(RestApiPath.ROOMS) +@RequiredArgsConstructor +public class RoomController +{ + + private final RestCache cache; + private final ExecutableService executableService; + + @Operation(summary = "Lists rooms (executables).") + @GetMapping + public ListResponse listRooms( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @RequestParam(value = "start", required = false) Integer start, + @RequestParam(value = "count", required = false) Integer count, + @RequestParam(value = "sort", required = false, defaultValue = "SLOT") ExecutableListRequest.Sort sort, + @RequestParam(value = "sort-desc", required = false, defaultValue = "true") boolean sortDescending, + @RequestParam(value = "room-id", required = false) String roomId, + @RequestParam(value = "participant-user-id", required = false) String participantUserId) + { + ExecutableListRequest request = new ExecutableListRequest(); + request.setSecurityToken(securityToken); + request.setStart(start); + request.setCount(count); + request.setSort(sort); + request.setSortDescending(sortDescending); + if (roomId != null) { + request.addType(ExecutableSummary.Type.USED_ROOM); + request.setRoomId(roomId); + } + else { + if (participantUserId != null) { + request.setRoomLicenseCount(ExecutableListRequest.FILTER_NON_ZERO); + request.addType(ExecutableSummary.Type.USED_ROOM); + } + request.addType(ExecutableSummary.Type.ROOM); + } + request.setParticipantUserId(participantUserId); + + ListResponse response = executableService.listExecutables(request); + ListResponse listResponse = new ListResponse<>(); + listResponse.addAll(response.getItems().stream().map(RoomModel::new).collect(Collectors.toList())); + listResponse.setStart(response.getStart()); + listResponse.setCount(response.getCount()); + return listResponse; + } + + @Operation(summary = "Gets room's (executable's) authorized data.") + @GetMapping(RestApiPath.ID_SUFFIX) + public RoomAuthorizedData getRoom( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id) + { + String roomId = cache.getExecutableId(securityToken, id); + AbstractRoomExecutable roomExecutable = + (AbstractRoomExecutable) executableService.getExecutable(securityToken, roomId); + + return new RoomAuthorizedData(roomExecutable); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/RuntimeController.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/RuntimeController.java new file mode 100644 index 000000000..e6c1f54cc --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/RuntimeController.java @@ -0,0 +1,241 @@ +package cz.cesnet.shongo.controller.rest.controllers; + +import cz.cesnet.shongo.api.MediaData; +import cz.cesnet.shongo.api.RoomParticipant; +import cz.cesnet.shongo.controller.api.ExecutionReport; +import cz.cesnet.shongo.controller.api.RecordingService; +import cz.cesnet.shongo.controller.api.SecurityToken; +import cz.cesnet.shongo.controller.api.request.ExecutableServiceListRequest; +import cz.cesnet.shongo.controller.api.request.ListResponse; +import cz.cesnet.shongo.controller.api.rpc.ExecutableService; +import cz.cesnet.shongo.controller.rest.CacheProvider; +import cz.cesnet.shongo.controller.rest.RestApiPath; +import cz.cesnet.shongo.controller.rest.RestCache; +import cz.cesnet.shongo.controller.rest.RoomCache; +import cz.cesnet.shongo.controller.rest.error.UnsupportedApiException; +import cz.cesnet.shongo.controller.rest.models.runtimemanagement.RuntimeParticipantModel; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static cz.cesnet.shongo.controller.rest.config.security.AuthFilter.TOKEN; + +/** + * Rest controller for runtime endpoints. + * + * @author Filip Karnis + */ +@Slf4j +@RestController +@RequestMapping(RestApiPath.RUNTIME_MANAGEMENT) +@RequiredArgsConstructor +public class RuntimeController +{ + + private final RestCache cache; + private final RoomCache roomCache; + private final ExecutableService executableService; + + @Operation(summary = "Lists reservation request runtime participants.") + @GetMapping(RestApiPath.RUNTIME_MANAGEMENT_PARTICIPANTS) + ListResponse listRuntimeParticipants( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id, + @RequestParam(required = false) Integer start, + @RequestParam(required = false) Integer count) + { + String executableId = cache.getExecutableId(securityToken, id); + CacheProvider cacheProvider = new CacheProvider(cache, securityToken); + List roomParticipants = Collections.emptyList(); + try { + roomParticipants = roomCache.getRoomParticipants(securityToken, executableId); + } + catch (Exception exception) { + log.warn("Failed to load participants", exception); + } + + List items = roomParticipants + .stream() + .map(roomParticipant -> new RuntimeParticipantModel(roomParticipant, cacheProvider)) + .collect(Collectors.toList()); + return ListResponse.fromRequest(start, count, items); + } + + @Operation(summary = "Takes snapshot of reservation request runtime participant.") + @PostMapping(RestApiPath.RUNTIME_MANAGEMENT_PARTICIPANTS_SNAPSHOT) + ResponseEntity snapshotRuntimeParticipant( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id, + @PathVariable String participantId) + { + String executableId = cache.getExecutableId(securityToken, id); + try { + MediaData participantSnapshot = roomCache.getRoomParticipantSnapshot( + securityToken, executableId, participantId); + if (participantSnapshot != null) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType(participantSnapshot.getType().toString())); + headers.setCacheControl("no-cache, no-store, must-revalidate"); + headers.setPragma("no-cache"); + return new ResponseEntity<>(participantSnapshot.getData(), headers, HttpStatus.OK); + } + } + catch (Exception exception) { + log.warn("Failed to get participant snapshot", exception); + } + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + @Operation(summary = "Modifies reservation request runtime participant.") + @PatchMapping(RestApiPath.RUNTIME_MANAGEMENT_PARTICIPANTS_MODIFY) + void modifyRuntimeParticipant( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id, + @PathVariable String participantId, + @RequestBody RuntimeParticipantModel body) + { + String executableId = cache.getExecutableId(securityToken, id); + RoomParticipant oldRoomParticipant = null; + if (!participantId.equals("*")) { + oldRoomParticipant = roomCache.getRoomParticipant(securityToken, executableId, participantId); + } + + // Parse body + String name = body.getName(); + Boolean microphoneEnabled = body.getMicrophoneEnabled(); + Integer microphoneLevel = body.getMicrophoneLevel(); + Boolean videoEnabled = body.getVideoEnabled(); + + RoomParticipant roomParticipant = new RoomParticipant(participantId); + if (name != null) { + roomParticipant.setDisplayName(name); + } + if (microphoneLevel != null) { + roomParticipant.setMicrophoneLevel(microphoneLevel); + } + if (microphoneEnabled != null) { + if (oldRoomParticipant != null && oldRoomParticipant.getMicrophoneEnabled() == null) { + throw new IllegalStateException("Mute microphone is not available."); + } + roomParticipant.setMicrophoneEnabled(microphoneEnabled); + } + if (videoEnabled != null) { + if (oldRoomParticipant != null && oldRoomParticipant.getVideoEnabled() == null) { + throw new IllegalStateException("Disable video is not available."); + } + roomParticipant.setVideoEnabled(videoEnabled); + } + if (participantId.equals("*")) { + roomParticipant.setId((String) null); + roomCache.modifyRoomParticipants(securityToken, executableId, roomParticipant); + } + else { + roomCache.modifyRoomParticipant(securityToken, executableId, roomParticipant); + } + } + + @Operation(summary = "Disconnects reservation request runtime participant.") + @PostMapping(RestApiPath.RUNTIME_MANAGEMENT_PARTICIPANTS_DISCONNECT) + void disconnectRuntimeParticipant( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id, + @PathVariable String participantId) + { + String executableId = cache.getExecutableId(securityToken, id); + roomCache.disconnectRoomParticipant(securityToken, executableId, participantId); + } + + @Operation(summary = "Starts recording of reservation request runtime.") + @PostMapping(RestApiPath.RUNTIME_MANAGEMENT_RECORDING_START) + void startRequestRecording( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id) + { + String executableId = cache.getExecutableId(securityToken, id); + String executableServiceId = getExecutableServiceId(securityToken, executableId); + + Object result = null; + try { + result = executableService.activateExecutableService(securityToken, executableId, executableServiceId); + } + catch (Exception exception) { + log.warn("Start recording failed", exception); + } + if (Boolean.TRUE.equals(result) || Boolean.FALSE.equals(result)) { + cache.clearExecutable(executableId); + } + else { + String errorCode = "startingFailed"; + if (result instanceof ExecutionReport) { + ExecutionReport executionReport = (ExecutionReport) result; + log.warn("Start recording failed: {}", executionReport); + + // Detect further error + ExecutionReport.UserError userError = executionReport.toUserError(); + if (userError instanceof ExecutionReport.RecordingUnavailable) { + errorCode = "unavailable"; + } + } + throw new RuntimeException("Starting recording failed with error: " + errorCode); + } + } + + @Operation(summary = "Stops recording of reservation request runtime.") + @PostMapping(RestApiPath.RUNTIME_MANAGEMENT_RECORDING_STOP) + void stopRequestRecording( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id) + { + String executableId = cache.getExecutableId(securityToken, id); + String executableServiceId = getExecutableServiceId(securityToken, executableId); + + Object result = null; + try { + result = executableService.deactivateExecutableService(securityToken, executableId, executableServiceId); + } + catch (Exception exception) { + log.warn("Stop recording failed", exception); + } + if (Boolean.TRUE.equals(result)) { + cache.clearExecutable(executableId); + } + else { + if (result instanceof ExecutionReport) { + ExecutionReport executionReport = (ExecutionReport) result; + log.warn("Stop recording failed: {}", executionReport); + } + } + cache.clearExecutable(executableId); + } + + private String getExecutableServiceId(SecurityToken securityToken, String executableId) + { + ExecutableServiceListRequest request = new ExecutableServiceListRequest(securityToken, executableId, RecordingService.class); + List services = executableService.listExecutableServices(request).getItems(); + log.debug("Found recording services: {}", services); + if (services.size() > 1) { + throw new UnsupportedApiException("Room " + executableId + " has multiple recording services."); + } + if (!services.isEmpty()) { + return services.get(0).getId(); + } + return null; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/TagController.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/TagController.java new file mode 100644 index 000000000..5f6bfa45d --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/TagController.java @@ -0,0 +1,40 @@ +package cz.cesnet.shongo.controller.rest.controllers; + +import cz.cesnet.shongo.controller.api.SecurityToken; +import cz.cesnet.shongo.controller.api.Tag; +import cz.cesnet.shongo.controller.api.request.TagListRequest; +import cz.cesnet.shongo.controller.api.rpc.ResourceService; +import cz.cesnet.shongo.controller.rest.RestApiPath; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cz.cesnet.shongo.controller.rest.config.security.AuthFilter.TOKEN; + +@RestController +@RequestMapping(RestApiPath.TAGS) +@RequiredArgsConstructor +public class TagController { + + private final ResourceService resourceService; + + /** + * Lists {@link Tag}s. + */ + @Operation(summary = "Lists available tags.") + @GetMapping + List listTags( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @RequestAttribute(value = "resourceId", required = false) String resourceId + ) { + TagListRequest request = new TagListRequest(securityToken); + request.setResourceId(resourceId); + + return resourceService.listTags(request); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/UserController.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/UserController.java new file mode 100644 index 000000000..1613fef44 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/UserController.java @@ -0,0 +1,159 @@ +package cz.cesnet.shongo.controller.rest.controllers; + +import cz.cesnet.shongo.api.UserInformation; +import cz.cesnet.shongo.controller.SystemPermission; +import cz.cesnet.shongo.controller.api.Group; +import cz.cesnet.shongo.controller.api.SecurityToken; +import cz.cesnet.shongo.controller.api.UserSettings; +import cz.cesnet.shongo.controller.api.request.GroupListRequest; +import cz.cesnet.shongo.controller.api.request.ListResponse; +import cz.cesnet.shongo.controller.api.request.UserListRequest; +import cz.cesnet.shongo.controller.api.rpc.AuthorizationService; +import cz.cesnet.shongo.controller.rest.RestApiPath; +import cz.cesnet.shongo.controller.rest.RestCache; +import cz.cesnet.shongo.controller.rest.models.users.SettingsModel; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cz.cesnet.shongo.controller.rest.config.security.AuthFilter.TOKEN; + +/** + * Rest controller for user endpoints. + * + * @author Filip Karnis + */ +@RestController +@RequestMapping(RestApiPath.USERS_AND_GROUPS) +@RequiredArgsConstructor +public class UserController +{ + + private final AuthorizationService authorizationService; + private final RestCache cache; + + /** + * Handle request for list of {@link UserInformation}s which contains given {@code filter} text in any field. + * + * @param filter + * @return list of {@link UserInformation}s + */ + @Operation(summary = "Lists users.") + @GetMapping(RestApiPath.USERS_LIST) + public ListResponse getUsers( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @RequestParam(value = "filter", required = false) String filter, + @RequestParam(value = "groupId", required = false) String groupId) + { + UserListRequest request = new UserListRequest(); + request.setSecurityToken(securityToken); + request.setSearch(filter); + if (groupId != null) { + request.addGroupId(groupId); + } + + return authorizationService.listUsers(request); + } + + /** + * Handle request for {@link UserInformation} by given {@code userId}. + * + * @param userId + * @return {@link UserInformation} + */ + @Operation(summary = "Returns information about user.") + @GetMapping(RestApiPath.USERS_DETAIL) + public UserInformation getUser( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String userId) + { + return cache.getUserInformation(securityToken, userId); + } + + /** + * Returns user settings with user's system permissions by given {@link SecurityToken}. + * + * @return {@link SettingsModel} + */ + @Operation(summary = "Returns user's settings.") + @GetMapping(RestApiPath.SETTINGS) + public SettingsModel getUserSettings(@RequestAttribute(TOKEN) SecurityToken securityToken) + { + UserSettings settings = authorizationService.getUserSettings(securityToken); + // TODO: Find a better place to authorize the user as an administrator + authorizationService.updateUserSettings(securityToken, settings); + List permissions = cache.getSystemPermissions(securityToken); + return new SettingsModel(settings, permissions); + } + + /** + * Handle request for updating user's settings. + * + * @param newSettings new settings of user + */ + @Operation(summary = "Updates user's settings.") + @PutMapping(RestApiPath.SETTINGS) + public SettingsModel updateUserSettings( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @RequestBody UserSettings newSettings) + { + UserSettings userSettings = authorizationService.getUserSettings(securityToken); + + boolean useWebService = newSettings.isUseWebService(); + userSettings.setUseWebService(newSettings.isUseWebService()); + if (!useWebService) { + userSettings.setLocale(newSettings.getLocale()); + userSettings.setHomeTimeZone(newSettings.getHomeTimeZone()); + } + userSettings.setCurrentTimeZone(newSettings.getCurrentTimeZone()); + userSettings.setAdministrationMode(newSettings.getAdministrationMode()); + + authorizationService.updateUserSettings(securityToken, userSettings); + cache.clearUserPermissions(securityToken); + + return getUserSettings(securityToken); + } + + /** + * Handle request for list of {@link Group}s which contains given {@code filter} text in any field. + * + * @param filter + * @return list of {@link Group}s + */ + @Operation(summary = "Lists groups.") + @GetMapping(RestApiPath.GROUPS_LIST) + public ListResponse getGroups( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @RequestParam(value = "filter", required = false) String filter) + { + GroupListRequest request = new GroupListRequest(); + request.setSecurityToken(securityToken); + request.setSearch(filter); + + return authorizationService.listGroups(request); + } + + /** + * Handle request for {@link Group} by given {@code groupId}. + * + * @param groupId + * @return {@link Group} + */ + @Operation(summary = "Returns information about group.") + @GetMapping(RestApiPath.GROUPS_DETAIL) + public Group getGroup( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String groupId) + { + return cache.getGroup(securityToken, groupId); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/UserRoleController.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/UserRoleController.java new file mode 100644 index 000000000..7a8b6578c --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/controllers/UserRoleController.java @@ -0,0 +1,102 @@ +package cz.cesnet.shongo.controller.rest.controllers; + +import cz.cesnet.shongo.controller.ObjectRole; +import cz.cesnet.shongo.controller.api.AclEntry; +import cz.cesnet.shongo.controller.api.SecurityToken; +import cz.cesnet.shongo.controller.api.request.AclEntryListRequest; +import cz.cesnet.shongo.controller.api.request.ListResponse; +import cz.cesnet.shongo.controller.api.rpc.AuthorizationService; +import cz.cesnet.shongo.controller.rest.CacheProvider; +import cz.cesnet.shongo.controller.rest.RestApiPath; +import cz.cesnet.shongo.controller.rest.RestCache; +import cz.cesnet.shongo.controller.rest.error.LastOwnerRoleNotDeletableException; +import cz.cesnet.shongo.controller.rest.models.roles.UserRoleModel; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.stream.Collectors; + +import static cz.cesnet.shongo.controller.rest.config.security.AuthFilter.TOKEN; + +/** + * Rest controller for roles endpoints. + * + * @author Filip Karnis + */ +@RestController +@RequestMapping(RestApiPath.ROLES) +@RequiredArgsConstructor +public class UserRoleController +{ + + private final AuthorizationService authorizationService; + private final RestCache cache; + + @Operation(summary = "Lists reservation request roles.") + @GetMapping + ListResponse listRequestRoles( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id, + @RequestParam(value = "start", required = false) Integer start, + @RequestParam(value = "count", required = false) Integer count) + { + AclEntryListRequest request = new AclEntryListRequest(); + request.setSecurityToken(securityToken); + request.setStart(start); + request.setCount(count); + request.addObjectId(id); + ListResponse aclEntries = authorizationService.listAclEntries(request); + + CacheProvider cacheProvider = new CacheProvider(cache, securityToken); + List items = aclEntries.getItems() + .stream() + .map(item -> new UserRoleModel(item, cacheProvider)) + .collect(Collectors.toList()); + return ListResponse.fromRequest(start, count, items); + } + + @ResponseStatus(HttpStatus.CREATED) + @Operation(summary = "Creates new role for reservation request.") + @PostMapping + void createRequestRoles( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id, + @RequestBody UserRoleModel userRoleModel) + { + String reservationRequestId = cache.getReservationRequestId(securityToken, id); + userRoleModel.setObjectId(reservationRequestId); + userRoleModel.setDeletable(true); + authorizationService.createAclEntry(securityToken, userRoleModel.toApi()); + } + + @Operation(summary = "Deletes role for reservation request.") + @DeleteMapping(RestApiPath.ENTITY_SUFFIX) + void deleteRequestRoles( + @RequestAttribute(TOKEN) SecurityToken securityToken, + @PathVariable String id, + @PathVariable String entityId) + { + String reservationRequestId = cache.getReservationRequestId(securityToken, id); + AclEntryListRequest request = new AclEntryListRequest(); + request.setSecurityToken(securityToken); + request.addObjectId(reservationRequestId); + request.addRole(ObjectRole.OWNER); + ListResponse aclEntries = authorizationService.listAclEntries(request); + if (aclEntries.getItemCount() == 1 && aclEntries.getItem(0).getId().equals(entityId)) { + throw new LastOwnerRoleNotDeletableException(); + } + authorizationService.deleteAclEntry(securityToken, entityId); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/error/ControllerExceptionHandler.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/error/ControllerExceptionHandler.java new file mode 100644 index 000000000..e421a9c2d --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/error/ControllerExceptionHandler.java @@ -0,0 +1,77 @@ +package cz.cesnet.shongo.controller.rest.error; + +import cz.cesnet.shongo.CommonReportSet; +import cz.cesnet.shongo.TodoImplementException; +import cz.cesnet.shongo.controller.ControllerReportSet.ReservationRequestDeletedException; +import cz.cesnet.shongo.controller.ControllerReportSet.ReservationRequestNotDeletableException; +import cz.cesnet.shongo.controller.ControllerReportSet.SecurityInvalidTokenException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.mail.MessagingException; + +/** + * Handler for exceptions thrown while processing Rest request. + * + * @author Filip Karnis + */ +@RestControllerAdvice +public class ControllerExceptionHandler +{ + + @ExceptionHandler(SecurityInvalidTokenException.class) + public ResponseEntity handleSecurityInvalidToken(SecurityInvalidTokenException e) + { + return ErrorModel.createResponseFromException(e, HttpStatus.UNAUTHORIZED); + } + + @ExceptionHandler(TodoImplementException.class) + public ResponseEntity handleTodo(TodoImplementException e) + { + return ErrorModel.createResponseFromException(e, HttpStatus.NOT_IMPLEMENTED); + } + + @ExceptionHandler(UnsupportedOperationException.class) + public ResponseEntity handleUnsupportedOperation(UnsupportedOperationException e) + { + return ErrorModel.createResponseFromException(e, HttpStatus.NOT_IMPLEMENTED); + } + + @ExceptionHandler(MessagingException.class) + public ResponseEntity handleMassaging(MessagingException e) + { + return ErrorModel.createResponseFromException(e, HttpStatus.SERVICE_UNAVAILABLE); + } + + @ExceptionHandler(LastOwnerRoleNotDeletableException.class) + public ResponseEntity handleLastOwnerRoleNotDeletable(LastOwnerRoleNotDeletableException e) + { + return ErrorModel.createResponseFromException(e, HttpStatus.FORBIDDEN); + } + + @ExceptionHandler(ReservationRequestNotDeletableException.class) + public ResponseEntity handleReservationRequestNotDeletable(ReservationRequestNotDeletableException e) + { + return ErrorModel.createResponseFromException(e, HttpStatus.FORBIDDEN); + } + + @ExceptionHandler(ObjectInaccessibleException.class) + public ResponseEntity handleObjectInaccessible(ObjectInaccessibleException e) + { + return ErrorModel.createResponseFromException(e, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(ReservationRequestDeletedException.class) + public ResponseEntity handleReservationRequestDeleted(ReservationRequestDeletedException e) + { + return ErrorModel.createResponseFromException(e, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(CommonReportSet.ObjectNotExistsException.class) + public ResponseEntity handleObjectNotExists(CommonReportSet.ObjectNotExistsException e) + { + return ErrorModel.createResponseFromException(e, HttpStatus.NOT_FOUND); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/error/ErrorModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/error/ErrorModel.java new file mode 100644 index 000000000..487404ca2 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/error/ErrorModel.java @@ -0,0 +1,23 @@ +package cz.cesnet.shongo.controller.rest.error; + +import lombok.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +/** + * Represents an error for rest error response. + * + * @author Filip Karnis + */ +@Value +public class ErrorModel +{ + + String error; + + public static ResponseEntity createResponseFromException(Exception e, HttpStatus httpStatus) + { + ErrorModel errorModel = new ErrorModel(e.getMessage()); + return new ResponseEntity<>(errorModel, httpStatus); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/error/LastOwnerRoleNotDeletableException.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/error/LastOwnerRoleNotDeletableException.java new file mode 100644 index 000000000..55f2900dc --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/error/LastOwnerRoleNotDeletableException.java @@ -0,0 +1,14 @@ +package cz.cesnet.shongo.controller.rest.error; + +/** + * Last owner role is not deletable. + * + * @author Filip Karnis + */ +public class LastOwnerRoleNotDeletableException extends RuntimeException +{ + public LastOwnerRoleNotDeletableException() + { + super("Last OWNER role cannot be deleted."); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/error/ObjectInaccessibleException.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/error/ObjectInaccessibleException.java new file mode 100644 index 000000000..2f2e1d045 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/error/ObjectInaccessibleException.java @@ -0,0 +1,21 @@ +package cz.cesnet.shongo.controller.rest.error; + +/** + * Object is inaccessible. + * + * @author Martin Srom + */ +public class ObjectInaccessibleException extends RuntimeException +{ + private final String objectId; + + public ObjectInaccessibleException(String objectId) + { + this.objectId = objectId; + } + + public String getObjectId() + { + return objectId; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/error/UnsupportedApiException.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/error/UnsupportedApiException.java new file mode 100644 index 000000000..e55cc8da8 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/error/UnsupportedApiException.java @@ -0,0 +1,30 @@ +package cz.cesnet.shongo.controller.rest.error; + +/** + * Unsupported API data. + * + * @author Martin Srom + */ +public class UnsupportedApiException extends RuntimeException +{ + public UnsupportedApiException(Object object) + { + super(object.getClass().getCanonicalName()); + } + + public UnsupportedApiException(String message) + { + super(message); + } + + public UnsupportedApiException(String message, Object... arguments) + { + super(String.format(message, arguments)); + } + + @Override + public String getMessage() + { + return String.format("Not supported: %s", super.getMessage()); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/CommonModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/CommonModel.java new file mode 100644 index 000000000..8c8ed9fdb --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/CommonModel.java @@ -0,0 +1,50 @@ +package cz.cesnet.shongo.controller.rest.models; + +/** + * Common validation methods. + * + * @author Martin Srom + */ +public class CommonModel +{ + /** + * Prefix for new unique identifiers. + */ + private static final String NEW_ID_PREFIX = "new-"; + + /** + * Last auto-generated identifier index. + */ + private static int lastGeneratedId = 0; + + /** + * @param id to be checked + * @return true whether given {@code id} is auto-generated, false otherwise + */ + public synchronized static boolean isNewId(String id) + { + return id.startsWith(NEW_ID_PREFIX); + } + + /** + * @return new auto-generated identifier + */ + public synchronized static String getNewId() + { + return NEW_ID_PREFIX + ++lastGeneratedId; + } + + /** + * @param string + * @return given {@code string} which can be used in double quoted string (e.g., "") + */ + public static String escapeDoubleQuotedString(String string) + { + if (string == null) { + return null; + } + string = string.replaceAll("\n", "\\\\n"); + string = string.replaceAll("\"", "\\\\\""); + return string; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/IdModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/IdModel.java new file mode 100644 index 000000000..b25f04d6d --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/IdModel.java @@ -0,0 +1,13 @@ +package cz.cesnet.shongo.controller.rest.models; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IdModel { + + private String id; +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/TechnologyModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/TechnologyModel.java new file mode 100644 index 000000000..22af5d013 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/TechnologyModel.java @@ -0,0 +1,86 @@ +package cz.cesnet.shongo.controller.rest.models; + +import cz.cesnet.shongo.Technology; +import lombok.Getter; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Technology of the alias/room reservation request or executable. + * + * @author Martin Srom + * @author Filip Karnis + */ +@Getter +public enum TechnologyModel +{ + + PEXIP("views.technologyModel.PEXIP", Technology.H323, Technology.SIP, + Technology.SKYPE_FOR_BUSINESS, Technology.RTMP, + Technology.WEBRTC), + + /** + * {@link Technology#ADOBE_CONNECT} + */ + ADOBE_CONNECT("views.technologyModel.ADOBE_CONNECT", Technology.ADOBE_CONNECT), + + /** + * {@link Technology#FREEPBX} + */ + FREEPBX("views.technologyModel.FREEPBX", Technology.FREEPBX), + /** + * {@link Technology#H323} and/or {@link Technology#SIP} + */ + H323_SIP("views.technologyModel.H323_SIP", Technology.H323, Technology.SIP); + + + /** + * Code of the title which can be displayed to user. + */ + private final String titleCode; + + /** + * Set of {@link Technology}s which it represents. + */ + private final Set technologies; + + /** + * Constructor. + * + * @param titleCode sets the {@link #titleCode} + * @param technologies sets the {@link #technologies} + */ + TechnologyModel(String titleCode, Technology... technologies) + { + this.titleCode = titleCode; + Set technologySet = new HashSet<>(Arrays.asList(technologies)); + this.technologies = Collections.unmodifiableSet(technologySet); + } + + /** + * @param technologies which must the returned {@link TechnologyModel} contain + * @return {@link TechnologyModel} which contains all given {@code technologies} + */ + public static TechnologyModel find(Set technologies) + { + if (technologies.size() == 0) { + return null; + } + if (H323_SIP.technologies.containsAll(technologies)) { + return H323_SIP; + } + else if (ADOBE_CONNECT.technologies.containsAll(technologies)) { + return ADOBE_CONNECT; + } + else if (FREEPBX.technologies.containsAll(technologies)) { + return FREEPBX; + } + else if (PEXIP.technologies.containsAll(technologies)) { + return PEXIP; + } + return null; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/TimeInterval.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/TimeInterval.java new file mode 100644 index 000000000..57e7a8d48 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/TimeInterval.java @@ -0,0 +1,31 @@ +package cz.cesnet.shongo.controller.rest.models; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.joda.time.DateTime; +import org.joda.time.Interval; + +/** + * Represents {@link Interval} for REST API. + * + * @author Filip Karnis + */ +@Data +public class TimeInterval +{ + + public static final String ISO_8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = ISO_8601_PATTERN) + private DateTime start; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = ISO_8601_PATTERN) + private DateTime end; + + public static TimeInterval fromApi(Interval interval) + { + TimeInterval timeInterval = new TimeInterval(); + timeInterval.setStart(interval.getStart()); + timeInterval.setEnd(interval.getEnd()); + return timeInterval; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/participant/ParticipantConfigurationModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/participant/ParticipantConfigurationModel.java new file mode 100644 index 000000000..e8f16e798 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/participant/ParticipantConfigurationModel.java @@ -0,0 +1,86 @@ +package cz.cesnet.shongo.controller.rest.models.participant; + +import cz.cesnet.shongo.ParticipantRole; +import lombok.extern.slf4j.Slf4j; + +import java.util.LinkedList; +import java.util.List; + +/** + * Configuration of {@link ParticipantModel}s. + * + * @author Martin Srom + */ +@Slf4j +public class ParticipantConfigurationModel +{ + + /** + * List of participants. + */ + protected List participants = new LinkedList<>(); + + + /** + * @return {@link #participants} + */ + public List getParticipants() + { + return participants; + } + + + /** + * @param participant to be added to the {@link #participants} + */ + public void addParticipant(ParticipantModel participant) + { + if (participant.getType().equals(ParticipantModel.Type.USER)) { + String userId = participant.getUserId(); + for (ParticipantModel existingParticipant : participants) { + String existingUserId = existingParticipant.getUserId(); + ParticipantModel.Type existingType = existingParticipant.getType(); + if (existingType.equals(ParticipantModel.Type.USER) && existingUserId.equals(userId)) { + ParticipantRole existingRole = existingParticipant.getRole(); + if (existingRole.compareTo(participant.getRole()) >= 0) { + log.warn("Skip adding {} because {} already exists.", participant, existingParticipant); + return; + } + else { + log.warn("Removing {} because {} will be added.", existingParticipant, participant); + participants.remove(existingParticipant); + } + break; + } + } + } + participants.add(participant); + } + + /** + * @param participant to be removed from the {@link #participants} + */ + public void removeParticipant(ParticipantModel participant) + { + for (ParticipantModel existingParticipant : participants) { + if (participant.getId().equals(existingParticipant.getId())) { + participants.remove(existingParticipant); + break; + } + } + } + + /** + * @param participant to be removed from the {@link #participants} + */ + public ParticipantModel getParticipant(ParticipantModel participant) + { + for (ParticipantModel existingParticipant : participants) { + if (participant.getId().equals(existingParticipant.getId())) { + participants.remove(existingParticipant); + return participant; + } + } + return null; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/participant/ParticipantModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/participant/ParticipantModel.java new file mode 100644 index 000000000..c24c9b47b --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/participant/ParticipantModel.java @@ -0,0 +1,128 @@ +package cz.cesnet.shongo.controller.rest.models.participant; + +import cz.cesnet.shongo.ParticipantRole; +import cz.cesnet.shongo.TodoImplementException; +import cz.cesnet.shongo.api.UserInformation; +import cz.cesnet.shongo.controller.api.AbstractParticipant; +import cz.cesnet.shongo.controller.api.AbstractPerson; +import cz.cesnet.shongo.controller.api.AnonymousPerson; +import cz.cesnet.shongo.controller.api.PersonParticipant; +import cz.cesnet.shongo.controller.api.UserPerson; +import cz.cesnet.shongo.controller.rest.CacheProvider; +import cz.cesnet.shongo.controller.rest.error.UnsupportedApiException; +import cz.cesnet.shongo.controller.rest.models.CommonModel; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Model for {@link AbstractParticipant}. + * + * @author Martin Srom + * @author Filip Karnis + */ +@Data +@NoArgsConstructor +public class ParticipantModel +{ + + protected String id; + + private Type type; + + private String userId; + + private String name; + + private String email; + + private ParticipantRole role; + + private String organization; + + public ParticipantModel(UserInformation userInformation) + { + this.type = Type.USER; + setUserId(userInformation.getUserId()); + } + + public ParticipantModel(AbstractParticipant participant, CacheProvider cacheProvider) + { + this.id = participant.getId(); + if (participant instanceof PersonParticipant) { + PersonParticipant personParticipant = (PersonParticipant) participant; + this.role = personParticipant.getRole(); + AbstractPerson person = personParticipant.getPerson(); + if (person instanceof UserPerson) { + UserPerson userPerson = (UserPerson) person; + setType(Type.USER); + setUserId(userPerson.getUserId()); + UserInformation userInformation = cacheProvider.getUserInformation(userPerson.getUserId()); + setName(userInformation.getFullName()); + setEmail(userInformation.getEmail()); + setOrganization(userInformation.getOrganization()); + } + else if (person instanceof AnonymousPerson) { + AnonymousPerson anonymousPerson = (AnonymousPerson) person; + type = Type.ANONYMOUS; + name = anonymousPerson.getName(); + email = anonymousPerson.getEmail(); + } + else { + throw new UnsupportedApiException(person.getClass()); + } + } + else { + throw new UnsupportedApiException(participant.getClass()); + } + } + + public AbstractParticipant toApi() + { + PersonParticipant personParticipant = new PersonParticipant(); + if (!isNew()) { + personParticipant.setId(id); + } + personParticipant.setRole(role); + switch (type) { + case USER: { + UserPerson userPerson = new UserPerson(); + if (userId == null) { + throw new IllegalStateException("User must not be null."); + } + userPerson.setUserId(userId); + personParticipant.setPerson(userPerson); + return personParticipant; + } + case ANONYMOUS: { + AnonymousPerson anonymousPerson = new AnonymousPerson(); + anonymousPerson.setName(name); + anonymousPerson.setEmail(email); + personParticipant.setPerson(anonymousPerson); + return personParticipant; + } + default: + throw new TodoImplementException(type); + } + } + + private boolean isNew() + { + return id == null || CommonModel.isNewId(id); + } + + public void setNewId() + { + this.id = CommonModel.getNewId(); + } + + public void setNullId() + { + this.id = null; + } + + public enum Type + { + USER, + ANONYMOUS + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/recording/RecordingModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/recording/RecordingModel.java new file mode 100644 index 000000000..4d16ca8e9 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/recording/RecordingModel.java @@ -0,0 +1,53 @@ +package cz.cesnet.shongo.controller.rest.models.recording; + +import com.fasterxml.jackson.annotation.JsonFormat; +import cz.cesnet.shongo.controller.api.ResourceRecording; +import lombok.Data; +import org.joda.time.DateTime; +import org.joda.time.Duration; + +import static cz.cesnet.shongo.controller.rest.models.TimeInterval.ISO_8601_PATTERN; + +/** + * Represents a recording. + * + * @author Filip Karnis + */ +@Data +public class RecordingModel +{ + + private String id; + private String name; + private String description; + private String resourceId; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = ISO_8601_PATTERN) + private DateTime beginDate; + private Long duration; + private Boolean isPublic; + private String downloadUrl; + private String viewUrl; + private String editUrl; + private String filename; + + public RecordingModel(ResourceRecording recording) + { + this.id = recording.getId(); + this.name = recording.getName(); + this.description = recording.getDescription(); + this.resourceId = recording.getResourceId(); + this.beginDate = recording.getBeginDate(); + Duration duration = recording.getDuration(); + if (duration == null || duration.isShorterThan(Duration.standardMinutes(1))) { + this.duration = null; + } + else { + this.duration = duration.getMillis(); + } + this.isPublic = recording.isPublic(); + this.downloadUrl = recording.getDownloadUrl(); + this.viewUrl = recording.getViewUrl(); + this.editUrl = recording.getEditUrl(); + this.filename = recording.getFileName(); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/report/MetaModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/report/MetaModel.java new file mode 100644 index 000000000..3bc30fb6a --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/report/MetaModel.java @@ -0,0 +1,16 @@ +package cz.cesnet.shongo.controller.rest.models.report; + +import cz.cesnet.shongo.controller.rest.models.users.SettingsModel; +import lombok.Data; +import org.joda.time.DateTimeZone; + +/** + * Represents meta data for {@link ReportModel}. + */ +@Data +public class MetaModel +{ + + private DateTimeZone timeZone; + private SettingsModel settings; +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/report/ReportModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/report/ReportModel.java new file mode 100644 index 000000000..89289b431 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/report/ReportModel.java @@ -0,0 +1,35 @@ +package cz.cesnet.shongo.controller.rest.models.report; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +/** + * Represents a report problem model. + * + * @author Martin Srom + * @author Filip Karnis + */ +@Slf4j +@Data +public class ReportModel +{ + + private String email; + + private String message; + + private String emailSubject = "Problem report"; + + /** + * Meta information about the report. + */ + private MetaModel meta; + + public String getEmailContent() + { + return "From: " + getEmail() + "\n\n" + + message + "\n\n" + + "--------------------------------------------------------------------------------\n\n" + + meta; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationdevice/ReservationDeviceModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationdevice/ReservationDeviceModel.java new file mode 100644 index 000000000..b006689a1 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationdevice/ReservationDeviceModel.java @@ -0,0 +1,20 @@ +package cz.cesnet.shongo.controller.rest.models.reservationdevice; + +import cz.cesnet.shongo.controller.api.ReservationDevice; +import lombok.Data; + +/** + * Represents a reservation device with authorization to create/view reservations of a particular resource. + * + * @author Michal Drobňák + */ +@Data +public class ReservationDeviceModel { + private String id; + private String resourceId; + + public ReservationDeviceModel(ReservationDevice reservationDevice) { + this.id = reservationDevice.getId(); + this.resourceId = reservationDevice.getResourceId(); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/PeriodicityModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/PeriodicityModel.java new file mode 100644 index 000000000..fbe1a06f1 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/PeriodicityModel.java @@ -0,0 +1,49 @@ +package cz.cesnet.shongo.controller.rest.models.reservationrequest; + +import cz.cesnet.shongo.controller.api.PeriodicDateTimeSlot; +import lombok.Data; +import org.joda.time.LocalDate; + +import java.util.List; + +/** + * Represents periodicity of reservation. + * + * @author Filip Karnis + */ +@Data +public class PeriodicityModel +{ + + /** + * Type of the period + */ + private PeriodicDateTimeSlot.PeriodicityType type; + + /** + * Cycle of the period + */ + private int periodicityCycle; + + /** + * Days of the period for weekly period + */ + private PeriodicDateTimeSlot.DayOfWeek[] periodicDaysInWeek; + + /** + * End of the period + */ + private LocalDate periodicityEnd; + + /** + * Dates excluded from period + */ + private List excludeDates; + + /** + * Periodicity parameters for specific day in month (e.g. 2. friday in a month) + */ + private PeriodicDateTimeSlot.PeriodicityType.MonthPeriodicityType monthPeriodicityType; + private PeriodicDateTimeSlot.DayOfWeek periodicityDayInMonth; + private int periodicityDayOrder; +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/PhysicalResourceData.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/PhysicalResourceData.java new file mode 100644 index 000000000..e6eb844e0 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/PhysicalResourceData.java @@ -0,0 +1,26 @@ +package cz.cesnet.shongo.controller.rest.models.reservationrequest; + +import cz.cesnet.shongo.controller.api.ResourceSummary; +import lombok.Data; + +@Data +public class PhysicalResourceData +{ + + private String resourceId; + private String resourceName; + private String resourceDescription; + private String periodicity; + + public static PhysicalResourceData fromApi(ResourceSummary summary) + { + if (summary == null) { + return null; + } + PhysicalResourceData physicalResourceData = new PhysicalResourceData(); + physicalResourceData.setResourceId(summary.getId()); + physicalResourceData.setResourceName(summary.getName()); + physicalResourceData.setResourceDescription(summary.getDescription()); + return physicalResourceData; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/ReservationRequestCreateModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/ReservationRequestCreateModel.java new file mode 100644 index 000000000..307d7df20 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/ReservationRequestCreateModel.java @@ -0,0 +1,1365 @@ +package cz.cesnet.shongo.controller.rest.models.reservationrequest; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Strings; +import cz.cesnet.shongo.AliasType; +import cz.cesnet.shongo.ParticipantRole; +import cz.cesnet.shongo.Temporal; +import cz.cesnet.shongo.TodoImplementException; +import cz.cesnet.shongo.api.AdobeConnectPermissions; +import cz.cesnet.shongo.api.AdobeConnectRoomSetting; +import cz.cesnet.shongo.api.FreePBXRoomSetting; +import cz.cesnet.shongo.api.H323RoomSetting; +import cz.cesnet.shongo.api.PexipRoomSetting; +import cz.cesnet.shongo.api.RoomSetting; +import cz.cesnet.shongo.api.UserInformation; +import cz.cesnet.shongo.controller.ObjectPermission; +import cz.cesnet.shongo.controller.ObjectRole; +import cz.cesnet.shongo.controller.ReservationRequestPurpose; +import cz.cesnet.shongo.controller.ReservationRequestReusement; +import cz.cesnet.shongo.controller.api.AbstractParticipant; +import cz.cesnet.shongo.controller.api.AbstractReservationRequest; +import cz.cesnet.shongo.controller.api.AbstractRoomExecutable; +import cz.cesnet.shongo.controller.api.AclEntry; +import cz.cesnet.shongo.controller.api.AliasSpecification; +import cz.cesnet.shongo.controller.api.AuxiliaryData; +import cz.cesnet.shongo.controller.api.ExecutableServiceSpecification; +import cz.cesnet.shongo.controller.api.ExecutableState; +import cz.cesnet.shongo.controller.api.PeriodicDateTimeSlot; +import cz.cesnet.shongo.controller.api.RecordingServiceSpecification; +import cz.cesnet.shongo.controller.api.Reservation; +import cz.cesnet.shongo.controller.api.ReservationRequest; +import cz.cesnet.shongo.controller.api.ReservationRequestSet; +import cz.cesnet.shongo.controller.api.ReservationRequestSummary; +import cz.cesnet.shongo.controller.api.ReservationRequestType; +import cz.cesnet.shongo.controller.api.ResourceSpecification; +import cz.cesnet.shongo.controller.api.ResourceSummary; +import cz.cesnet.shongo.controller.api.RoomAvailability; +import cz.cesnet.shongo.controller.api.RoomEstablishment; +import cz.cesnet.shongo.controller.api.RoomExecutableParticipantConfiguration; +import cz.cesnet.shongo.controller.api.RoomSpecification; +import cz.cesnet.shongo.controller.api.SecurityToken; +import cz.cesnet.shongo.controller.api.Specification; +import cz.cesnet.shongo.controller.api.request.AclEntryListRequest; +import cz.cesnet.shongo.controller.api.request.ListResponse; +import cz.cesnet.shongo.controller.api.request.ReservationRequestListRequest; +import cz.cesnet.shongo.controller.api.rpc.AuthorizationService; +import cz.cesnet.shongo.controller.api.rpc.ReservationService; +import cz.cesnet.shongo.controller.rest.CacheProvider; +import cz.cesnet.shongo.controller.rest.RestCache; +import cz.cesnet.shongo.controller.rest.error.UnsupportedApiException; +import cz.cesnet.shongo.controller.rest.models.TechnologyModel; +import cz.cesnet.shongo.controller.rest.models.TimeInterval; +import cz.cesnet.shongo.controller.rest.models.participant.ParticipantModel; +import cz.cesnet.shongo.controller.rest.models.roles.UserRoleModel; +import cz.cesnet.shongo.util.SlotHelper; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.joda.time.DateTime; +import org.joda.time.DateTimeField; +import org.joda.time.DateTimeFieldType; +import org.joda.time.DateTimeZone; +import org.joda.time.Duration; +import org.joda.time.Interval; +import org.joda.time.LocalDate; +import org.joda.time.LocalDateTime; +import org.joda.time.LocalTime; +import org.joda.time.Partial; +import org.joda.time.Period; +import org.joda.time.ReadablePartial; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import static cz.cesnet.shongo.controller.rest.models.TimeInterval.ISO_8601_PATTERN; + +/** + * Model for {@link AbstractReservationRequest}. + * + * @author Martin Srom + * @author Filip Karnis + */ +@Slf4j +@Data +public class ReservationRequestCreateModel +{ + + protected String id; + + protected String parentReservationRequestId; + + @JsonProperty("requestType") + protected ReservationRequestType type; + + protected String description; + + protected ReservationRequestPurpose purpose = ReservationRequestPurpose.USER; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = ISO_8601_PATTERN) + protected DateTime dateTime; + + protected TechnologyModel technology; + + protected DateTimeZone timeZone = DateTimeZone.UTC; + + protected Integer durationCount; + + protected DurationType durationType; + + protected int slotBeforeMinutes; + + protected int slotAfterMinutes; + + protected PeriodicityModel periodicity; + + /** + * Exclude date from #excludeDates for highligting in user GUI + */ + protected LocalDate removedReservationDate; + + @JsonProperty("type") + protected SpecificationType specificationType; + + protected String roomName; + + protected String e164Number; + + protected String roomReservationRequestId; + + protected ReservationRequestSummary permanentRoomReservationRequest; + + protected Integer participantCount; + + protected String userPin; + + protected String adminPin; + + protected String guestPin; + + protected boolean record; + + protected String roomRecordingResourceId; + + protected AdobeConnectPermissions roomAccessMode; + + protected List userRoles = new LinkedList<>(); + + protected List roomParticipants = new LinkedList<>(); + + protected boolean roomParticipantNotificationEnabled = false; + + protected String roomMeetingName; + + protected String roomMeetingDescription; + + protected Interval collidingInterval; + + protected boolean collidingWithFirstSlot = false; + + protected boolean allowGuests = false; + + @JsonProperty("resource") + protected String resourceId; + + private TimeInterval slot; + + @JsonIgnore + private CacheProvider cacheProvider; + + List auxData = new ArrayList<>(); + + /** + * Create new {@link ReservationRequestModel} from scratch. + */ + public ReservationRequestCreateModel() + { + periodicity = new PeriodicityModel(); + periodicity.setType(PeriodicDateTimeSlot.PeriodicityType.NONE); + periodicity.setPeriodicityCycle(1); + slot = new TimeInterval(); + slot.setStart(Temporal.roundDateTimeToMinutes(DateTime.now(), 1)); + } + + /** + * Create new {@link ReservationRequestModel} from scratch. + */ + public ReservationRequestCreateModel(CacheProvider cacheProvider) + { + this(); + this.cacheProvider = cacheProvider; + } + + /** + * Create new {@link ReservationRequestModel} from existing {@code reservationRequest}. + * + * @param reservationRequest + */ + public ReservationRequestCreateModel(AbstractReservationRequest reservationRequest, CacheProvider cacheProvider) + { + this(cacheProvider); + fromApi(reservationRequest, cacheProvider); + + // Load permanent room + if (specificationType.equals(SpecificationType.ROOM_CAPACITY) && cacheProvider != null) { + loadPermanentRoom(cacheProvider); + } + } + + /** + * @param reservationService + * @param securityToken + * @param cache + * @return list of reservation requests for permanent rooms + */ + public static List getPermanentRooms( + ReservationService reservationService, + SecurityToken securityToken, + RestCache cache) + { + ReservationRequestListRequest request = new ReservationRequestListRequest(); + request.setSecurityToken(securityToken); + request.addSpecificationType(ReservationRequestSummary.SpecificationType.PERMANENT_ROOM); + List reservationRequests = new LinkedList<>(); + + ListResponse response = reservationService.listReservationRequests(request); + if (response.getItemCount() > 0) { + Set reservationRequestIds = new HashSet<>(); + for (ReservationRequestSummary reservationRequestSummary : response) { + reservationRequestIds.add(reservationRequestSummary.getId()); + } + cache.fetchObjectPermissions(securityToken, reservationRequestIds); + + for (ReservationRequestSummary reservationRequestSummary : response) { + ExecutableState executableState = reservationRequestSummary.getExecutableState(); + if (executableState == null || (!executableState.isAvailable() && !executableState.equals( + ExecutableState.NOT_STARTED))) { + continue; + } + Set objectPermissions = cache.getObjectPermissions(securityToken, + reservationRequestSummary.getId()); + if (!objectPermissions.contains(ObjectPermission.PROVIDE_RESERVATION_REQUEST)) { + continue; + } + reservationRequests.add(reservationRequestSummary); + } + } + return reservationRequests; + } + + /** + * @param reservationRequestId + * @param reservationService + * @param securityToken + * @return list of deletion dependencies for reservation request with given {@code reservationRequestId} + */ + public static List getDeleteDependencies( + String reservationRequestId, + ReservationService reservationService, + SecurityToken securityToken) + { + // List reservation requests which reuse the reservation request to be deleted + ReservationRequestListRequest reservationRequestListRequest = new ReservationRequestListRequest(); + reservationRequestListRequest.setSecurityToken(securityToken); + reservationRequestListRequest.setReusedReservationRequestId(reservationRequestId); + ListResponse reservationRequests = + reservationService.listReservationRequests(reservationRequestListRequest); + return reservationRequests.getItems(); + } + + public LocalTime getStart() + { + return slot.getStart().toLocalTime(); + } + + public DateTime getRequestStart() + { + return slot.getStart(); + } + + public DateTime getEnd() + { + return slot.getEnd(); + } + + public void setEnd(DateTime end) + { + slot.setEnd(end); + } + + public void setCollidingInterval(Interval collidingInterval) + { + this.collidingInterval = collidingInterval; + + collidingWithFirstSlot = false; + DateTime start = getRequestStart(); + if (getEnd() == null) { + switch (this.durationType) { + case MINUTE: + collidingWithFirstSlot = SlotHelper.areIntervalsColliding(start, this.durationCount, 0, 0, + collidingInterval); + break; + case HOUR: + collidingWithFirstSlot = SlotHelper.areIntervalsColliding(start, 0, this.durationCount, 0, + collidingInterval); + break; + case DAY: + collidingWithFirstSlot = SlotHelper.areIntervalsColliding(start, 0, 0, this.durationCount, + collidingInterval); + break; + } + } + else { + collidingWithFirstSlot = SlotHelper.areIntervalsColliding(start, getEnd(), collidingInterval); + } + } + + public void resetCollidingInterval() + { + collidingInterval = null; + collidingWithFirstSlot = false; + } + + public String getMeetingRoomResourceName() + { + if (resourceId != null) { + ResourceSummary resourceSummary = cacheProvider.getResourceSummary(resourceId); + if (resourceSummary != null) { + return resourceSummary.getName(); + } + } + return null; + } + + public String getMeetingRoomResourceDescription() + { + ResourceSummary resourceSummary = cacheProvider.getResourceSummary(resourceId); + if (resourceSummary != null) { + return resourceSummary.getDescription(); + } + return null; + } + + public String getMeetingRoomResourceDomain() + { + if (resourceId != null) { + ResourceSummary resourceSummary = cacheProvider.getResourceSummary(resourceId); + if (resourceSummary != null) { + return resourceSummary.getDomainName(); + } + } + return null; + } + + public Period getSlotBefore() + { + if (slotBeforeMinutes != 0) { + return Period.minutes(slotBeforeMinutes); + } + else { + return null; + } + } + + public Period getSlotAfter() + { + if (slotAfterMinutes != 0) { + return Period.minutes(slotAfterMinutes); + } + else { + return null; + } + } + + public LocalDate getStartDate() + { + return slot.getStart().toLocalDate(); + } + + public void setStartDate(LocalDate startDate) + { + this.slot.setStart(startDate.toDateTimeAtStartOfDay()); + } + + public void setRoomReservationRequestId(String roomReservationRequestId) + { + if (roomReservationRequestId == null + || !roomReservationRequestId.equals(this.roomReservationRequestId)) { + this.permanentRoomReservationRequest = null; + } + this.roomReservationRequestId = roomReservationRequestId; + } + + public void setPermanentRoomReservationRequestId(String permanentRoomReservationRequestId, + List permanentRooms) + { + this.roomReservationRequestId = permanentRoomReservationRequestId; + + if (permanentRoomReservationRequestId != null && getStart() != null) { + for (ReservationRequestSummary permanentRoomSummary : permanentRooms) { + if (permanentRoomSummary.getId().equals(permanentRoomReservationRequestId)) { + Reservation reservation = cacheProvider.getReservation(permanentRoomSummary.getLastReservationId()); + Interval permanentRoomSlot = reservation.getSlot(); + DateTime permanentRoomStart = permanentRoomSlot.getStart().plus(getSlotBefore()); + permanentRoomStart = Temporal.roundDateTimeToMinutes(permanentRoomStart, 1); + if (getRequestStart().isBefore(permanentRoomStart)) { + setStartDate(permanentRoomStart.toLocalDate()); + } + break; + } + } + } + } + + public String getRoomResourceName() + { + ResourceSummary resourceSummary = cacheProvider.getResourceSummary(resourceId); + if (resourceSummary != null) { + return resourceSummary.getName(); + } + return null; + } + + public String getRoomRecordingResourceName() + { + ResourceSummary resource = cacheProvider.getResourceSummary(roomRecordingResourceId); + if (resource != null) { + return resource.getName(); + } + else { + return null; + } + } + + public void setRoomAccessMode(AdobeConnectPermissions roomAccessMode) + { + AdobeConnectPermissions.checkIfUsableByMeetings(roomAccessMode); + this.roomAccessMode = roomAccessMode; + } + + public UserRoleModel getUserRole(String userRoleId) + { + if (userRoleId == null) { + return null; + } + for (UserRoleModel userRole : userRoles) { + if (userRoleId.equals(userRole.getId())) { + return userRole; + } + } + return null; + } + + public UserRoleModel addUserRole(UserInformation userInformation, ObjectRole objectRole) + { + UserRoleModel userRole = new UserRoleModel(userInformation); + userRole.setObjectId(id); + userRole.setRole(objectRole); + userRoles.add(userRole); + return userRole; + } + + public void addUserRole(UserRoleModel userRole) + { + userRoles.add(userRole); + } + + public void removeUserRole(UserRoleModel userRole) + { + userRoles.remove(userRole); + } + + public void addRoomParticipant(ParticipantModel participantModel) + { + if (ParticipantModel.Type.USER.equals(participantModel.getType())) { + String userId = participantModel.getUserId(); + for (ParticipantModel existingParticipant : roomParticipants) { + String existingUserId = existingParticipant.getUserId(); + ParticipantModel.Type existingType = existingParticipant.getType(); + if (existingType.equals(ParticipantModel.Type.USER) && existingUserId.equals(userId)) { + ParticipantRole existingRole = existingParticipant.getRole(); + if (existingRole.compareTo(participantModel.getRole()) >= 0) { + log.warn("Skip adding {} because {} already exists.", participantModel, existingParticipant); + return; + } + else { + log.warn("Removing {} because {} will be added.", existingParticipant, participantModel); + roomParticipants.remove(existingParticipant); + } + break; + } + } + } + roomParticipants.add(participantModel); + } + + public ParticipantModel addRoomParticipant(UserInformation userInformation, ParticipantRole role) + { + ParticipantModel participantModel = new ParticipantModel(userInformation); + participantModel.setNewId(); + participantModel.setRole(role); + participantModel.setEmail(userInformation.getEmail()); + participantModel.setName(userInformation.getFullName()); + participantModel.setOrganization(userInformation.getOrganization()); + addRoomParticipant(participantModel); + return participantModel; + } + + public void clearRoomParticipants() + { + roomParticipants.clear(); + } + + /** + * Load attributes from given {@code specification}. + * + * @param specification + */ + public void fromSpecificationApi(Specification specification, CacheProvider cacheProvider) + { + if (specification instanceof RoomSpecification) { + RoomSpecification roomSpecification = (RoomSpecification) specification; + + for (RoomSetting roomSetting : roomSpecification.getRoomSettings()) { + if (roomSetting instanceof H323RoomSetting) { + H323RoomSetting h323RoomSetting = (H323RoomSetting) roomSetting; + if (h323RoomSetting.getPin() != null) { + try { + String pin = h323RoomSetting.getPin(); + if (!pin.isEmpty()) { + userPin = String.valueOf(Integer.parseInt(pin)); + } + } + catch (NumberFormatException exception) { + log.warn("Failed parsing pin", exception); + } + } + } + if (roomSetting instanceof AdobeConnectRoomSetting) { + AdobeConnectRoomSetting adobeConnectRoomSetting = (AdobeConnectRoomSetting) roomSetting; + userPin = adobeConnectRoomSetting.getPin(); + roomAccessMode = adobeConnectRoomSetting.getAccessMode(); + } + if (roomSetting instanceof PexipRoomSetting) { + PexipRoomSetting pexipRoomSetting = (PexipRoomSetting) roomSetting; + guestPin = pexipRoomSetting.getGuestPin(); + adminPin = pexipRoomSetting.getHostPin(); + allowGuests = pexipRoomSetting.getAllowGuests(); + } + } + roomParticipants.clear(); + for (AbstractParticipant participant : roomSpecification.getParticipants()) { + roomParticipants.add(new ParticipantModel(participant, cacheProvider)); + } + + RoomEstablishment roomEstablishment = roomSpecification.getEstablishment(); + if (roomEstablishment != null) { + technology = TechnologyModel.find(roomEstablishment.getTechnologies()); + resourceId = roomEstablishment.getResourceId(); + + AliasSpecification roomNameAlias = + roomEstablishment.getAliasSpecificationByType(AliasType.ROOM_NAME); + AliasSpecification e164NumberAlias = + roomEstablishment.getAliasSpecificationByType(AliasType.H323_E164); + if (roomNameAlias != null) { + roomName = roomNameAlias.getValue(); + } + if (e164NumberAlias != null) { + e164Number = e164NumberAlias.getValue(); + } + } + + RoomAvailability roomAvailability = roomSpecification.getAvailability(); + if (roomAvailability != null) { + slotBeforeMinutes = roomAvailability.getSlotMinutesBefore(); + slotAfterMinutes = roomAvailability.getSlotMinutesAfter(); + participantCount = roomAvailability.getParticipantCount(); + roomParticipantNotificationEnabled = roomAvailability.isParticipantNotificationEnabled(); + roomMeetingName = roomAvailability.getMeetingName(); + roomMeetingDescription = roomAvailability.getMeetingDescription(); + for (ExecutableServiceSpecification service : roomAvailability.getServiceSpecifications()) { + if (service instanceof RecordingServiceSpecification) { + record = service.isEnabled(); + roomRecordingResourceId = service.getResourceId(); + } + } + } + + if (roomEstablishment != null) { + specificationType = SpecificationType.VIRTUAL_ROOM; + } + else { + specificationType = SpecificationType.ROOM_CAPACITY; + } + } + else if (specification instanceof ResourceSpecification) { + ResourceSpecification resourceSpecification = (ResourceSpecification) specification; + resourceId = resourceSpecification.getResourceId(); + ReservationRequestSummary summary = cacheProvider.getAllocatedReservationRequestSummary(this.id); + specificationType = SpecificationType.fromReservationRequestSummary(summary, true); + } + else { + throw new UnsupportedApiException(specification); + } + } + + /** + * Load attributes from given {@code abstractReservationRequest}. + * + * @param abstractReservationRequest from which the attributes should be loaded + */ + public void fromApi(AbstractReservationRequest abstractReservationRequest, CacheProvider cacheProvider) + { + id = abstractReservationRequest.getId(); + type = abstractReservationRequest.getType(); + dateTime = abstractReservationRequest.getDateTime(); + description = abstractReservationRequest.getDescription(); + auxData = abstractReservationRequest.getAuxData(); + + // Specification + Specification specification = abstractReservationRequest.getSpecification(); + fromSpecificationApi(specification, cacheProvider); + if (SpecificationType.ROOM_CAPACITY.equals(specificationType)) { + roomReservationRequestId = abstractReservationRequest.getReusedReservationRequestId(); + } + + // Date/time slot and periodicity + Period duration = null; + if (abstractReservationRequest instanceof ReservationRequest) { + ReservationRequest reservationRequest = (ReservationRequest) abstractReservationRequest; + periodicity.setType(PeriodicDateTimeSlot.PeriodicityType.NONE); + Interval slot = reservationRequest.getSlot(); + this.slot.setStart(slot.getStart()); + this.slot.setEnd(slot.getEnd()); + duration = slot.toPeriod(); + + parentReservationRequestId = reservationRequest.getParentReservationRequestId(); + } + else if (abstractReservationRequest instanceof ReservationRequestSet) { + if (specificationType.equals(SpecificationType.VIRTUAL_ROOM)) { + throw new UnsupportedApiException("Periodicity is not allowed for permanent rooms."); + } + + ReservationRequestSet reservationRequestSet = (ReservationRequestSet) abstractReservationRequest; + List slots = reservationRequestSet.getSlots(); + boolean periodicityEndSet = false; + + int index = 0; + // Multiple slots awailable only for WEEKLY periodicity, check done few lines lower + periodicity.setPeriodicDaysInWeek(new PeriodicDateTimeSlot.DayOfWeek[slots.size()]); + // Set slot properties and periodicity + for (Object slot : slots) { + if (!(slot instanceof PeriodicDateTimeSlot)) { + throw new UnsupportedApiException("Only periodic date/time slots are allowed."); + } + PeriodicDateTimeSlot periodicSlot = (PeriodicDateTimeSlot) slot; + PeriodicDateTimeSlot.PeriodicityType periodicityType = + PeriodicDateTimeSlot.PeriodicityType.fromPeriod(periodicSlot.getPeriod()); + int periodicityCycle = PeriodicDateTimeSlot.PeriodicityType.getPeriodCycle(periodicSlot.getPeriod()); + + // Allows multiple slots only for WEEKLY + if (!PeriodicDateTimeSlot.PeriodicityType.WEEKLY.equals(periodicityType) && slots.size() != 1) { + throw new UnsupportedApiException( + "Multiple periodic date/time slots are allowed only for week period."); + } + // Check if all slots have the same periodicity + if (periodicity.getType() == null) { + periodicity.setType(periodicityType); + periodicity.setPeriodicityCycle(periodicityCycle); + timeZone = periodicSlot.getTimeZone(); + duration = periodicSlot.getDuration(); + } + else if (!periodicity.getType() + .equals(periodicityType) || periodicity.getPeriodicityCycle() != periodicityCycle + || !periodicSlot.getDuration().equals(duration) || !timeZone.equals( + periodicSlot.getTimeZone())) { + throw new UnsupportedApiException( + "Multiple periodic date/time slots with different periodicity are not allowed."); + } + + int dayIndex = (periodicSlot.getStart().getDayOfWeek() == 7 ? 1 : periodicSlot.getStart() + .getDayOfWeek() + 1); + if (getStartDate() == null || getStartDate().isAfter(periodicSlot.getStart().toLocalDate())) { + this.slot.setStart(periodicSlot.getStart().toDateTime(timeZone)); + this.slot.setEnd(periodicSlot.getStart().plus(duration)); + } + periodicity.getPeriodicDaysInWeek()[index] = PeriodicDateTimeSlot.DayOfWeek.fromDayIndex(dayIndex); + index++; + + if (PeriodicDateTimeSlot.PeriodicityType.MONTHLY.equals(periodicityType) + && periodicity.getMonthPeriodicityType() == null) { + periodicity.setMonthPeriodicityType(periodicSlot.getMonthPeriodicityType()); + if (PeriodicDateTimeSlot.PeriodicityType.MonthPeriodicityType.SPECIFIC_DAY.equals( + periodicity.getMonthPeriodicityType())) { + periodicity.setPeriodicityDayOrder(periodicSlot.getPeriodicityDayOrder()); + periodicity.setPeriodicityDayInMonth(periodicSlot.getPeriodicityDayInMonth()); + } + } + + ReadablePartial slotEnd = periodicSlot.getEnd(); + LocalDate periodicityEnd; + if (slotEnd == null) { + periodicityEnd = null; + } + else if (slotEnd instanceof LocalDate) { + periodicityEnd = (LocalDate) slotEnd; + } + else if (slotEnd instanceof Partial) { + Partial partial = (Partial) slotEnd; + DateTimeField[] partialFields = partial.getFields(); + if (!(partialFields.length == 3 + && partial.isSupported(DateTimeFieldType.year()) + && partial.isSupported(DateTimeFieldType.monthOfYear()) + && partial.isSupported(DateTimeFieldType.dayOfMonth()))) { + throw new UnsupportedApiException("Slot end %s.", slotEnd); + } + periodicityEnd = new LocalDate(partial.getValue(0), partial.getValue(1), partial.getValue(2)); + } + else { + throw new UnsupportedApiException("Slot end %s.", slotEnd); + } + + if (!periodicityEndSet) { + periodicity.setPeriodicityEnd(periodicityEnd); + periodicityEndSet = true; + } + else if ((periodicity.getPeriodicityEnd() == null && periodicity.getPeriodicityEnd() != periodicityEnd) + || (periodicity.getPeriodicityEnd() != null && !periodicity.getPeriodicityEnd() + .equals(periodicityEnd))) { + throw new UnsupportedApiException("Slot end %s is not same for all slots.", slotEnd); + } + + // Set exclude dates for slot + if (periodicSlot.getExcludeDates() != null) { + if (periodicity.getExcludeDates() == null) { + periodicity.setExcludeDates(new LinkedList<>()); + } + periodicity.getExcludeDates().addAll(periodicSlot.getExcludeDates()); + // Remove duplicates + periodicity.setExcludeDates(new ArrayList<>(new HashSet<>(periodicity.getExcludeDates()))); + } + } + } + else { + throw new UnsupportedApiException(abstractReservationRequest); + } + + // Room duration + if (!specificationType.equals(SpecificationType.VIRTUAL_ROOM)) { + setDuration(duration); + } + } + + /** + * Load {@link #permanentRoomReservationRequest} by given {@code cacheProvider}. + * + * @param cacheProvider + */ + public ReservationRequestSummary loadPermanentRoom(CacheProvider cacheProvider) + { + if (StringUtils.isEmpty(roomReservationRequestId)) { + throw new UnsupportedApiException("Permanent room capacity should have permanent room set."); + } + permanentRoomReservationRequest = cacheProvider.getAllocatedReservationRequestSummary(roomReservationRequestId); + roomName = permanentRoomReservationRequest.getRoomName(); + technology = TechnologyModel.find(permanentRoomReservationRequest.getSpecificationTechnologies()); + addPermanentRoomParticipants(); + return permanentRoomReservationRequest; + } + + /** + * Store attributes to {@link Specification}. + * + * @return {@link Specification} with stored attributes + */ + public Specification toSpecificationApi() + { + Specification specification; + switch (specificationType) { + case VIRTUAL_ROOM: { + RoomSpecification roomSpecification = new RoomSpecification(); + // Room establishment + RoomEstablishment roomEstablishment = roomSpecification.createEstablishment(); + roomEstablishment.setTechnologies(technology.getTechnologies()); + AliasSpecification roomNameSpecification = new AliasSpecification(); + roomNameSpecification.addTechnologies(technology.getTechnologies()); + roomNameSpecification.addAliasType(AliasType.ROOM_NAME); + roomNameSpecification.setValue(roomName); + roomEstablishment.addAliasSpecification(roomNameSpecification); + if (technology.equals(TechnologyModel.H323_SIP) || technology.equals(TechnologyModel.PEXIP)) { + AliasSpecification e164NumberSpecification = new AliasSpecification(AliasType.H323_E164); + if (!Strings.isNullOrEmpty(e164Number)) { + e164NumberSpecification.setValue(e164Number); + } + roomEstablishment.addAliasSpecification(e164NumberSpecification); + } + specification = roomSpecification; + break; + } + case ROOM_CAPACITY: { + RoomSpecification roomSpecification = new RoomSpecification(); + // Room availability + RoomAvailability roomAvailability = roomSpecification.createAvailability(); + roomAvailability.setParticipantCount(participantCount); + roomAvailability.setParticipantNotificationEnabled(roomParticipantNotificationEnabled); + roomAvailability.setMeetingName(roomMeetingName); + roomAvailability.setMeetingDescription(roomMeetingDescription); + if (record && !TechnologyModel.ADOBE_CONNECT.equals(technology)) { + roomAvailability.addServiceSpecification(new RecordingServiceSpecification(true)); + } + specification = roomSpecification; + break; + } + case PHYSICAL_RESOURCE: + case VEHICLE: + case PARKING_PLACE: + case DEVICE: + case MEETING_ROOM: { + specification = new ResourceSpecification(resourceId); + break; + } + default: + throw new TodoImplementException(specificationType); + } + + if (specification instanceof RoomSpecification) { + RoomSpecification roomSpecification = (RoomSpecification) specification; + + for (ParticipantModel participant : roomParticipants) { + if (participant.getId() == null) { + continue; + } + roomSpecification.addParticipant(participant.toApi()); + } + + if (TechnologyModel.PEXIP.equals(technology)) { + PexipRoomSetting pexipRoomSetting = new PexipRoomSetting(); + if (!allowGuests && !Strings.isNullOrEmpty(guestPin)) { + throw new IllegalStateException("Guests must be allowed in order to set a guest pin."); + } + pexipRoomSetting.setHostPin(adminPin); + pexipRoomSetting.setGuestPin(guestPin); + pexipRoomSetting.setAllowGuests(allowGuests); + roomSpecification.addRoomSetting(pexipRoomSetting); + } + + if (TechnologyModel.FREEPBX.equals(technology)) { + FreePBXRoomSetting freePBXRoomSetting = new FreePBXRoomSetting(); + freePBXRoomSetting.setAdminPin(adminPin); + freePBXRoomSetting.setUserPin(userPin); + roomSpecification.addRoomSetting(freePBXRoomSetting); + } + + if (TechnologyModel.H323_SIP.equals(technology) && userPin != null) { + H323RoomSetting h323RoomSetting = new H323RoomSetting(); + h323RoomSetting.setPin(userPin); + roomSpecification.addRoomSetting(h323RoomSetting); + } + if (TechnologyModel.ADOBE_CONNECT.equals(technology)) { + AdobeConnectRoomSetting adobeConnectRoomSetting = new AdobeConnectRoomSetting(); + if (!Strings.isNullOrEmpty(userPin)) { + adobeConnectRoomSetting.setPin(userPin); + } + adobeConnectRoomSetting.setAccessMode(roomAccessMode); + roomSpecification.addRoomSetting(adobeConnectRoomSetting); + } + RoomEstablishment roomEstablishment = roomSpecification.getEstablishment(); + if (roomEstablishment != null) { + if (!Strings.isNullOrEmpty(resourceId)) { + roomEstablishment.setResourceId(resourceId); + } + } + RoomAvailability roomAvailability = roomSpecification.getAvailability(); + if (roomAvailability != null) { + roomAvailability.setSlotMinutesBefore(slotBeforeMinutes); + roomAvailability.setSlotMinutesAfter(slotAfterMinutes); + } + } + return specification; + } + + /** + * @return requested reservation duration as {@link Period} + */ + public Period getDuration() + { + return new Period(slot.getStart(), slot.getEnd()); + } + + /** + * @param duration + */ + public void setDuration(Period duration) + { + switch (specificationType) { + case PHYSICAL_RESOURCE: + case PARKING_PLACE: + case VEHICLE: + case DEVICE: + case MEETING_ROOM: + case ROOM_CAPACITY: + int minutes; + try { + minutes = duration.toStandardMinutes().getMinutes(); + } + catch (UnsupportedOperationException exception) { + throw new UnsupportedApiException(duration.toString(), exception); + } + if ((minutes % (60 * 24)) == 0) { + durationCount = minutes / (60 * 24); + durationType = DurationType.DAY; + } + else if ((minutes % 60) == 0) { + durationCount = minutes / 60; + durationType = DurationType.HOUR; + } + else { + durationCount = minutes; + durationType = DurationType.MINUTE; + } + break; + default: + throw new TodoImplementException(specificationType); + } + } + + /** + * @return requested first reservation date/time slot as {@link Interval} + */ + public Interval getFirstSlot() + { + PeriodicDateTimeSlot first = getSlots(timeZone).first(); + DateTime start = first.getStart(); + if (timeZone != null) { + // Use specified time zone + LocalDateTime localDateTime = start.toLocalDateTime(); + start = localDateTime.toDateTime(timeZone); + } + if (specificationType == SpecificationType.VIRTUAL_ROOM) { + return new Interval(getRequestStart().withTime(0, 0, 0, 0), getDuration()); + } + return new Interval(start, getDuration()); + } + + public Period getPeriod() + { + Period period = null; + switch (periodicity.getType()) { + case DAILY: + period = Period.days(1); + break; + case WEEKLY: + period = Period.weeks(periodicity.getPeriodicityCycle()); + break; + case MONTHLY: + period = Period.months(periodicity.getPeriodicityCycle()); + break; + } + return period; + } + + /** + * @return all calculated slots + */ + public SortedSet getSlots(DateTimeZone timeZone) + { + SortedSet slots = new TreeSet<>(); + if (PeriodicDateTimeSlot.PeriodicityType.NONE.equals(periodicity.getType())) { + DateTime requestStart = getRequestStart(); + if (SpecificationType.VIRTUAL_ROOM.equals(getSpecificationType())) { + requestStart = requestStart.withTimeAtStartOfDay(); + } + PeriodicDateTimeSlot periodicDateTimeSlot = + new PeriodicDateTimeSlot(requestStart, getDuration(), Period.ZERO); + periodicDateTimeSlot.setEnd(getStartDate()); + periodicDateTimeSlot.setTimeZone(getTimeZone()); + slots.add(periodicDateTimeSlot); + } + else { + // Determine period + Period period = periodicity.getType().toPeriod(periodicity.getPeriodicityCycle()); + + if (PeriodicDateTimeSlot.PeriodicityType.WEEKLY.equals(periodicity.getType())) { + for (PeriodicDateTimeSlot.DayOfWeek day : periodicity.getPeriodicDaysInWeek()) { + DateTime nextSlotStart = getRequestStart(); + int dayIndex = (day.getDayIndex() == 1 ? 7 : day.getDayIndex() - 1); + while (nextSlotStart.getDayOfWeek() != dayIndex) { + nextSlotStart = nextSlotStart.plusDays(1); + } + PeriodicDateTimeSlot periodicDateTimeSlot = new PeriodicDateTimeSlot(); + periodicDateTimeSlot.setStart(nextSlotStart); + if (this.timeZone != null) { + periodicDateTimeSlot.setTimeZone(this.timeZone); + } + else if (timeZone != null) { + periodicDateTimeSlot.setTimeZone(timeZone); + } + periodicDateTimeSlot.setDuration(getDuration()); + periodicDateTimeSlot.setPeriod(period); + periodicDateTimeSlot.setEnd(periodicity.getPeriodicityEnd()); + slots.add(periodicDateTimeSlot); + } + } + else { + PeriodicDateTimeSlot periodicDateTimeSlot = new PeriodicDateTimeSlot(); + periodicDateTimeSlot.setStart(getFirstSlotStart()); + if (this.timeZone != null) { + periodicDateTimeSlot.setTimeZone(this.timeZone); + } + else if (timeZone != null) { + periodicDateTimeSlot.setTimeZone(timeZone); + } + periodicDateTimeSlot.setDuration(getDuration()); + periodicDateTimeSlot.setPeriod(period); + periodicDateTimeSlot.setEnd(periodicity.getPeriodicityEnd()); + if (PeriodicDateTimeSlot.PeriodicityType.MONTHLY.equals(periodicity.getType())) { + if (PeriodicDateTimeSlot.PeriodicityType.MonthPeriodicityType.SPECIFIC_DAY.equals( + periodicity.getMonthPeriodicityType())) { + periodicDateTimeSlot.setMonthPeriodicityType(periodicity.getMonthPeriodicityType()); + periodicDateTimeSlot.setPeriodicityDayOrder(periodicity.getPeriodicityDayOrder()); + periodicDateTimeSlot.setPeriodicityDayInMonth(periodicity.getPeriodicityDayInMonth()); + } + else { + periodicDateTimeSlot.setMonthPeriodicityType(periodicity.getMonthPeriodicityType()); + } + } + slots.add(periodicDateTimeSlot); + } + } + + return Collections.unmodifiableSortedSet(slots); + } + + public DateTime getFirstSlotStart() + { + DateTime slotStart = getRequestStart(); + if (PeriodicDateTimeSlot.PeriodicityType.MONTHLY.equals(periodicity.getType()) + && PeriodicDateTimeSlot.PeriodicityType.MonthPeriodicityType.SPECIFIC_DAY.equals( + periodicity.getMonthPeriodicityType())) { + slotStart = getMonthFirstSlotStart(slotStart); + } + return slotStart; + } + + /** + * Calculate slot start when #periodicityType is MONTHLY and for SPECIFIC_DAY for given #slotStart + * + * @param slotStart DateTime from which start + * @return DateTime for specific date of month + */ + private DateTime getMonthFirstSlotStart(DateTime slotStart) + { + if (!PeriodicDateTimeSlot.PeriodicityType.MONTHLY.equals(periodicity.getType()) + || !PeriodicDateTimeSlot.PeriodicityType.MonthPeriodicityType.SPECIFIC_DAY.equals( + periodicity.getMonthPeriodicityType())) { + throw new IllegalStateException("Periodicity type has to be monthly for a specific day."); + } + if (periodicity.getPeriodicityDayInMonth() == null || + ( + periodicity.getPeriodicityDayOrder() != -1 && + (periodicity.getPeriodicityDayOrder() < 1 || periodicity.getPeriodicityDayOrder() > 4) + ) + || periodicity.getPeriodicityEnd() == null) { + throw new IllegalStateException("For periodicity type MONTHLY must be set day of month."); + } + + while (slotStart.getDayOfWeek() != (periodicity.getPeriodicityDayInMonth().getDayIndex() == 1 ? 7 + : periodicity.getPeriodicityDayInMonth().getDayIndex() - 1)) { + slotStart = slotStart.plusDays(1); + } + DateTime monthEnd = slotStart.plusMonths(1).minusDays(slotStart.getDayOfMonth() - 1); + if (0 < periodicity.getPeriodicityDayOrder() && periodicity.getPeriodicityDayOrder() < 5) { + while ((slotStart.getDayOfMonth() % 7 == 0 + ? slotStart.getDayOfMonth() / 7 + : slotStart.getDayOfMonth() / 7 + 1) != periodicity.getPeriodicityDayOrder()) { + if (slotStart.plusDays(7).isBefore(monthEnd.plusMonths(1))) { + slotStart = slotStart.plusDays(7); + } + } + } + else if (periodicity.getPeriodicityDayOrder() == -1) { + while (true) { + if (!slotStart.plusDays(7).isAfter(monthEnd.minusDays(1))) { + slotStart = slotStart.plusDays(7); + } + else { + break; + } + } + } + else { + throw new TodoImplementException(); + } + return slotStart; + } + + /** + * @param participantId + * @return {@link ParticipantModel} with given {@code participantId} + */ + public ParticipantModel getParticipant(String participantId) + { + ParticipantModel participant = null; + for (ParticipantModel possibleParticipant : roomParticipants) { + if (participantId.equals(possibleParticipant.getId())) { + participant = possibleParticipant; + } + } + if (participant == null) { + throw new IllegalArgumentException("Participant " + participantId + " doesn't exist."); + } + return participant; + } + + /** + * @param userId + * @param role + * @return true wheter {@link #roomParticipants} contains user participant with given {@code userId} and {@code role} + */ + public boolean hasUserParticipant(String userId, ParticipantRole role) + { + for (ParticipantModel participant : roomParticipants) { + if (participant.getType().equals(ParticipantModel.Type.USER) && participant.getUserId().equals(userId) + && role.equals(participant.getRole())) { + return true; + } + } + return false; + } + + /** + * Add new participant. + * + * @param participant + */ + public boolean createParticipant(ParticipantModel participant, SecurityToken securityToken) + { + participant.setNewId(); + addRoomParticipant(participant); + return true; + } + + /** + * Modify existing participant + * + * @param participantId + * @param participant + */ + public boolean modifyParticipant(String participantId, ParticipantModel participant, SecurityToken securityToken) + { + ParticipantModel oldParticipant = getParticipant(participantId); + roomParticipants.remove(oldParticipant); + addRoomParticipant(participant); + return true; + } + + /** + * Delete existing participant. + * + * @param participantId + */ + public void deleteParticipant(String participantId) + { + ParticipantModel participant = getParticipant(participantId); + roomParticipants.remove(participant); + } + + + public LocalDate getFirstFutureSlotStart() + { + DateTime slotStart = getRequestStart(); + Period duration = getDuration(); + + Period period = getPeriod(); + while (slotStart.plus(duration).isBeforeNow() && period != null) { + switch (periodicity.getType()) { + case WEEKLY: + if (periodicity.getPeriodicDaysInWeek().length > 1) { + Set daysOfWeek = new HashSet<>(); + for (PeriodicDateTimeSlot.DayOfWeek day : periodicity.getPeriodicDaysInWeek()) { + daysOfWeek.add(day.getDayIndex() == 1 ? 7 : day.getDayIndex() - 1); + } + while (!daysOfWeek.contains(slotStart.getDayOfWeek()) || slotStart.plus(duration) + .isBeforeNow()) { + slotStart = slotStart.plusDays(1); + } + } + else { + slotStart = slotStart.plus(period); + } + break; + case MONTHLY: + if (PeriodicDateTimeSlot.PeriodicityType.MonthPeriodicityType.SPECIFIC_DAY.equals( + periodicity.getMonthPeriodicityType())) { + slotStart = getMonthFirstSlotStart(slotStart.plus(period).withDayOfMonth(1)); + break; + } + else { + slotStart = slotStart.plus(period); + } + break; + case DAILY: + slotStart = slotStart.plus(period); + break; + default: + throw new TodoImplementException("Unsupported periodicity type: " + periodicity.getType()); + } + } + return slotStart.toLocalDate(); + } + + /** + * Store all attributes to {@link AbstractReservationRequest}. + * + * @return {@link AbstractReservationRequest} with stored attributes + */ + public AbstractReservationRequest toApi() + { + SortedSet slots = getSlots(DateTimeZone.UTC); + // Create reservation request + AbstractReservationRequest abstractReservationRequest; + if (specificationType == SpecificationType.VIRTUAL_ROOM) { + ReservationRequest reservationRequest = new ReservationRequest(); + PeriodicDateTimeSlot slot = slots.first(); + reservationRequest.setSlot(slot.getStart(), slot.getStart().plus(Duration.standardDays(730))); + abstractReservationRequest = reservationRequest; + } + else if (periodicity.getType() == PeriodicDateTimeSlot.PeriodicityType.NONE) { + // Create single reservation request + ReservationRequest reservationRequest = new ReservationRequest(); + PeriodicDateTimeSlot slot = slots.first(); + reservationRequest.setSlot(slot.getStart(), slot.getStart().plus(slot.getDuration())); + abstractReservationRequest = reservationRequest; + } + else { + // Create set of reservation requests + ReservationRequestSet reservationRequestSet = new ReservationRequestSet(); + reservationRequestSet.addAllSlots(slots); + if (periodicity.getExcludeDates() != null && !periodicity.getExcludeDates().isEmpty()) { + for (LocalDate excludeDate : periodicity.getExcludeDates()) { + for (PeriodicDateTimeSlot slot : slots) { + if (Temporal.dateFitsInterval(slot.getStart(), slot.getEnd(), excludeDate)) { + slot.addExcludeDate(excludeDate); + } + } + } + } + abstractReservationRequest = reservationRequestSet; + } + if (!Strings.isNullOrEmpty(id)) { + abstractReservationRequest.setId(id); + } + abstractReservationRequest.setPurpose(purpose); + abstractReservationRequest.setDescription(description); + if (specificationType.equals(SpecificationType.VIRTUAL_ROOM)) { + abstractReservationRequest.setReusement(ReservationRequestReusement.OWNED); + } + else if (specificationType.equals(SpecificationType.ROOM_CAPACITY)) { + abstractReservationRequest.setReusedReservationRequestId(roomReservationRequestId); + } + + // Create specification + Specification specification = toSpecificationApi(); + abstractReservationRequest.setSpecification(specification); + + // Set reservation request to be deleted by scheduler if foreign resource is specified + abstractReservationRequest.setIsSchedulerDeleted(!Strings.isNullOrEmpty(getMeetingRoomResourceDomain())); + + abstractReservationRequest.setAuxData(auxData); + + return abstractReservationRequest; + } + + /** + * Add all {@link ParticipantModel} from {@link #permanentRoomReservationRequest} to {@link #roomParticipants} + */ + private void addPermanentRoomParticipants() + { + if (permanentRoomReservationRequest == null) { + throw new IllegalStateException("Permanent room reservation request must not be null"); + } + String permanentRoomReservationId = permanentRoomReservationRequest.getAllocatedReservationId(); + Reservation permanentRoomReservation = cacheProvider.getReservation(permanentRoomReservationId); + String permanentRoomId = permanentRoomReservation.getExecutable().getId(); + AbstractRoomExecutable permanentRoom = (AbstractRoomExecutable) cacheProvider.getExecutable(permanentRoomId); + RoomExecutableParticipantConfiguration permanentRoomParticipants = permanentRoom.getParticipantConfiguration(); + + // Remove all participants without identifier (old permanent room participants) + roomParticipants.removeIf(roomParticipant -> roomParticipant.getId() == null); + // Add all permanent room participants + int index = 0; + for (AbstractParticipant participant : permanentRoomParticipants.getParticipants()) { + ParticipantModel roomParticipant = new ParticipantModel(participant, cacheProvider); + roomParticipant.setNullId(); + roomParticipants.add(index++, roomParticipant); + } + } + + /** + * Load user roles into this {@link ReservationRequestModel}. + * + * @param securityToken + * @param authorizationService + */ + public void loadUserRoles(SecurityToken securityToken, AuthorizationService authorizationService) + { + if (id == null) { + throw new IllegalStateException("Id mustn't be null."); + } + // Add user roles + AclEntryListRequest userRoleRequest = new AclEntryListRequest(); + userRoleRequest.setSecurityToken(securityToken); + userRoleRequest.addObjectId(id); + for (AclEntry aclEntry : authorizationService.listAclEntries(userRoleRequest)) { + addUserRole(new UserRoleModel(aclEntry, cacheProvider)); + } + } + + /** + * @return default automatically added {@link ParticipantRole} for owner + */ + public ParticipantRole getDefaultOwnerParticipantRole() + { + if (TechnologyModel.H323_SIP.equals(technology)) { + return ParticipantRole.PARTICIPANT; + } + else { + return ParticipantRole.ADMINISTRATOR; + } + } + + @Override + public String toString() + { + return "ReservationRequestModel{" + + "id='" + id + '\'' + + ", description='" + description + '\'' + + ", type=" + type + + ", dateTime=" + dateTime + + ", technology=" + technology + + ", start=" + getStart() + + ", end=" + getEnd() + + ", excludeDates=" + periodicity.getExcludeDates() + + ", roomName='" + roomName + '\'' + + ", resourceId='" + resourceId + '\'' + + '}'; + } + + /** + * Type of duration unit. + */ + public enum DurationType + { + MINUTE, + HOUR, + DAY + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/ReservationRequestDetailModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/ReservationRequestDetailModel.java new file mode 100644 index 000000000..a7ade477c --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/ReservationRequestDetailModel.java @@ -0,0 +1,60 @@ +package cz.cesnet.shongo.controller.rest.models.reservationrequest; + +import cz.cesnet.shongo.api.UserInformation; +import cz.cesnet.shongo.controller.ObjectPermission; +import cz.cesnet.shongo.controller.api.AbstractReservationRequest; +import cz.cesnet.shongo.controller.api.AllocationState; +import cz.cesnet.shongo.controller.api.ExecutableState; +import cz.cesnet.shongo.controller.api.ReservationRequest; +import cz.cesnet.shongo.controller.api.ReservationRequestSummary; +import cz.cesnet.shongo.controller.api.ResourceSummary; +import cz.cesnet.shongo.controller.rest.models.room.RoomAuthorizedData; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Represents reservation request's detail info. + * It contains additional information about reservation request + * including {@link RoomAuthorizedData} and {@link List} {@link ReservationRequestHistoryModel}. + * + * @author Filip Karnis + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ReservationRequestDetailModel extends ReservationRequestModel +{ + + private AllocationState allocationState; + private Collection> allocationStateReport; + private ExecutableState executableState; + private Boolean notifyParticipants; + private RoomAuthorizedData authorizedData; + private List history; + + public ReservationRequestDetailModel( + ReservationRequestSummary summary, + ReservationRequestSummary virtualRoomSummary, + Map> permissionsByReservationRequestId, + UserInformation ownerInformation, + AbstractReservationRequest abstractReservationRequest, + RoomAuthorizedData authorizedData, + List history, + ResourceSummary resourceSummary) + { + super(summary, virtualRoomSummary, permissionsByReservationRequestId, ownerInformation, resourceSummary); + + this.allocationState = summary.getAllocationState(); + this.executableState = summary.getExecutableState(); + this.authorizedData = authorizedData; + this.history = history; + if (abstractReservationRequest instanceof ReservationRequest) { + ReservationRequest reservationRequest = (ReservationRequest) abstractReservationRequest; + this.allocationStateReport = reservationRequest.getAllocationStateReport().getReports(); + } + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/ReservationRequestHistoryModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/ReservationRequestHistoryModel.java new file mode 100644 index 000000000..bf7e86d98 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/ReservationRequestHistoryModel.java @@ -0,0 +1,39 @@ +package cz.cesnet.shongo.controller.rest.models.reservationrequest; + +import com.fasterxml.jackson.annotation.JsonFormat; +import cz.cesnet.shongo.controller.api.AllocationState; +import cz.cesnet.shongo.controller.api.ReservationRequestSummary; +import cz.cesnet.shongo.controller.api.ReservationRequestType; +import cz.cesnet.shongo.controller.rest.CacheProvider; +import lombok.Data; +import org.joda.time.DateTime; + +import static cz.cesnet.shongo.controller.rest.models.TimeInterval.ISO_8601_PATTERN; + +/** + * Represents reservation request's history. + * + * @author Filip Karnis + */ +@Data +public class ReservationRequestHistoryModel +{ + + private String id; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = ISO_8601_PATTERN) + private DateTime createdAt; + private String createdBy; + private ReservationRequestType type; + private AllocationState allocationState; + private ReservationRequestState state; + + public ReservationRequestHistoryModel(ReservationRequestSummary summary, CacheProvider cacheProvider) + { + this.id = summary.getId(); + this.createdAt = summary.getDateTime(); + this.createdBy = cacheProvider.getUserInformation(summary.getUserId()).getFullName(); + this.type = summary.getType(); + this.allocationState = summary.getAllocationState(); + this.state = ReservationRequestState.fromApi(summary); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/ReservationRequestModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/ReservationRequestModel.java new file mode 100644 index 000000000..188d81bab --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/ReservationRequestModel.java @@ -0,0 +1,95 @@ +package cz.cesnet.shongo.controller.rest.models.reservationrequest; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.JsonNode; +import cz.cesnet.shongo.api.UserInformation; +import cz.cesnet.shongo.controller.ObjectPermission; +import cz.cesnet.shongo.controller.api.ReservationRequestSummary; +import cz.cesnet.shongo.controller.api.ResourceSummary; +import cz.cesnet.shongo.controller.rest.models.TimeInterval; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.joda.time.DateTime; + +import java.util.Map; +import java.util.Set; + +import static cz.cesnet.shongo.controller.rest.models.TimeInterval.ISO_8601_PATTERN; + +/** + * Represents {@link cz.cesnet.shongo.controller.api.AbstractReservationRequest}. + * + * @author Filip Karnis + */ +@Data +@NoArgsConstructor +public class ReservationRequestModel +{ + + private String id; + private String description; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = ISO_8601_PATTERN) + private DateTime createdAt; + private String parentRequestId; + private ReservationRequestState state; + private Boolean isWritable; + private Boolean isProvidable; + private String ownerName; + private String ownerEmail; + private TimeInterval slot; + private Boolean isDeprecated; + private SpecificationType type; + private VirtualRoomModel virtualRoomData; + private PhysicalResourceData physicalResourceData; + private RoomCapacityModel roomCapacityData; + private String lastReservationId; + private Integer futureSlotCount; + private JsonNode auxData; + + public ReservationRequestModel( + ReservationRequestSummary summary, + ReservationRequestSummary virtualRoomSummary, + ResourceSummary resourceSummary) + { + this.id = summary.getId(); + this.description = summary.getDescription(); + this.createdAt = summary.getDateTime(); + this.parentRequestId = summary.getParentReservationRequestId(); + this.state = ReservationRequestState.fromApi(summary); + this.slot = TimeInterval.fromApi(summary.getEarliestSlot()); + this.type = SpecificationType.fromReservationRequestSummary(summary, true); + this.virtualRoomData = new VirtualRoomModel(virtualRoomSummary); + this.physicalResourceData = PhysicalResourceData.fromApi(resourceSummary); + this.roomCapacityData = new RoomCapacityModel(summary); + this.lastReservationId = summary.getLastReservationId(); + this.futureSlotCount = summary.getFutureSlotCount(); + this.auxData = summary.getAuxData(); + + switch (state != null ? state : ReservationRequestState.ALLOCATED) { + case ALLOCATED_STARTED: + case ALLOCATED_STARTED_AVAILABLE: + this.isDeprecated = false; + break; + default: + this.isDeprecated = slot != null && slot.getEnd().isBeforeNow(); + break; + } + } + + public ReservationRequestModel( + ReservationRequestSummary summary, + ReservationRequestSummary virtualRoomSummary, + Map> permissionsByReservationRequestId, + UserInformation ownerInformation, + ResourceSummary resourceSummary) + { + this(summary, virtualRoomSummary, resourceSummary); + + this.ownerName = ownerInformation.getFullName(); + this.ownerEmail = ownerInformation.getEmail(); + + Set objectPermissions = permissionsByReservationRequestId.get(id); + this.isWritable = objectPermissions.contains(ObjectPermission.WRITE); + this.isProvidable = objectPermissions.contains(ObjectPermission.PROVIDE_RESERVATION_REQUEST); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/ReservationRequestState.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/ReservationRequestState.java new file mode 100644 index 000000000..c86f0a50a --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/ReservationRequestState.java @@ -0,0 +1,151 @@ +package cz.cesnet.shongo.controller.rest.models.reservationrequest; + +import cz.cesnet.shongo.controller.api.AllocationState; +import cz.cesnet.shongo.controller.api.ExecutableState; +import cz.cesnet.shongo.controller.api.ReservationRequestSummary; +import cz.cesnet.shongo.controller.api.ReservationRequestType; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Represents a reservation request state. + * + * @author Martin Srom + * @author Filip Karnis + */ +@Getter +@AllArgsConstructor +public enum ReservationRequestState +{ + /** + * Reservation request has not been confirmed yet, nor allocated by the scheduler yet. + */ + CONFIRM_AWAITING(false), + + /** + * Reservation request has not been allocated by the scheduler yet. + */ + NOT_ALLOCATED(false), + + /** + * Reservation request is allocated by the scheduler but the allocated executable has not been started yet. + */ + ALLOCATED(true), + + /** + * Reservation request is allocated by the scheduler and the allocated executable is started. + */ + ALLOCATED_STARTED(true), + + /** + * Reservation request is allocated by the scheduler and the allocated room is started but not available for participants to join. + */ + ALLOCATED_STARTED_NOT_AVAILABLE(true), + + /** + * Reservation request is allocated by the scheduler and the allocated room is started and available for participants to join. + */ + ALLOCATED_STARTED_AVAILABLE(true), + + /** + * Reservation request is allocated by the scheduler and the allocated executable has been started and stopped. + */ + ALLOCATED_FINISHED(true), + + /** + * Reservation request cannot be allocated by the scheduler or the starting of executable failed. + */ + FAILED(false), + + /** + * The reservation request has been denied. It won't be allocated. + */ + DENIED(false), + + /** + * Modification of reservation request cannot be allocated by the scheduler + * but some previous version of reservation request has been allocated and started. + */ + MODIFICATION_FAILED(false); + + /** + * Specifies whether reservation request is allocated. + */ + private final boolean allocated; + + public static ReservationRequestState fromApi(ReservationRequestSummary reservationRequest) + { + return fromApi(reservationRequest.getAllocationState(), reservationRequest.getExecutableState(), + reservationRequest.getUsageExecutableState(), reservationRequest.getType(), + SpecificationType.fromReservationRequestSummary(reservationRequest), + reservationRequest.getAllocatedReservationId()); + } + + /** + * @param allocationState + * @param executableState + * @param usageExecutableState + * @param reservationRequestType + * @param specificationType + * @param lastReservationId + * @return {@link ReservationRequestState} + */ + public static ReservationRequestState fromApi(AllocationState allocationState, ExecutableState executableState, + ExecutableState usageExecutableState, ReservationRequestType reservationRequestType, + SpecificationType specificationType, String lastReservationId) + { + if (allocationState == null) { + return null; + } + switch (allocationState) { + case ALLOCATED: + if (executableState != null) { + switch (specificationType) { + case VIRTUAL_ROOM: + switch (executableState) { + case STARTED: + if (usageExecutableState != null && usageExecutableState.isAvailable()) { + return ALLOCATED_STARTED_AVAILABLE; + } + else { + return ALLOCATED_STARTED_NOT_AVAILABLE; + } + case STOPPED: + case STOPPING_FAILED: + return ALLOCATED_FINISHED; + case STARTING_FAILED: + return FAILED; + default: + return ALLOCATED; + } + case ROOM_CAPACITY: + switch (executableState) { + case STARTED: + return ALLOCATED_STARTED; + case STOPPED: + case STOPPING_FAILED: + return ALLOCATED_FINISHED; + case STARTING_FAILED: + return FAILED; + default: + return ALLOCATED; + } + } + } + return ALLOCATED; + case ALLOCATION_FAILED: + if (reservationRequestType.equals(ReservationRequestType.MODIFIED) && lastReservationId != null) { + return MODIFICATION_FAILED; + } + else { + return FAILED; + } + case CONFIRM_AWAITING: + return CONFIRM_AWAITING; + case DENIED: + return DENIED; + default: + return NOT_ALLOCATED; + } + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/RoomCapacityModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/RoomCapacityModel.java new file mode 100644 index 000000000..a5b4894a9 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/RoomCapacityModel.java @@ -0,0 +1,28 @@ +package cz.cesnet.shongo.controller.rest.models.reservationrequest; + +import cz.cesnet.shongo.controller.api.ReservationRequestSummary; +import lombok.Data; + +/** + * Represents capacity information for {@link cz.cesnet.shongo.controller.api.RoomAvailability}. + * + * @author Filip Karnis + */ +@Data +public class RoomCapacityModel +{ + + private String roomReservationRequestId; + private Integer capacityParticipantCount; + private Boolean hasRoomRecordingService; + private Boolean hasRoomRecordings; + private Boolean isRecordingActive; + + public RoomCapacityModel(ReservationRequestSummary summary) + { + this.roomReservationRequestId = summary.getReusedReservationRequestId(); + this.capacityParticipantCount = summary.getRoomParticipantCount(); + this.hasRoomRecordingService = summary.hasRoomRecordingService(); + this.hasRoomRecordings = summary.hasRoomRecordings(); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/SpecificationType.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/SpecificationType.java new file mode 100644 index 000000000..db2cb65c2 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/SpecificationType.java @@ -0,0 +1,147 @@ +package cz.cesnet.shongo.controller.rest.models.reservationrequest; + +import cz.cesnet.shongo.TodoImplementException; +import cz.cesnet.shongo.controller.Controller; +import cz.cesnet.shongo.controller.ControllerConfiguration; +import cz.cesnet.shongo.controller.api.ReservationRequestSummary; +import cz.cesnet.shongo.controller.api.Tag; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Type of specification for a reservation request. + * + * @author Martin Srom + * @author Filip Karnis + */ +@Getter +@AllArgsConstructor +public enum SpecificationType +{ + /** + * For permanent room. + */ + VIRTUAL_ROOM(true, false), + + /** + * For room capacity. + */ + ROOM_CAPACITY(false, false), + + /** + * For physical resource. + */ + PHYSICAL_RESOURCE(false, true), + + /** + * For meeting room. + */ + MEETING_ROOM(false, true), + + /** + * For parking place. + */ + PARKING_PLACE(false, true), + + /** + * For vehicles. + */ + VEHICLE(false, true), + + /** + * For device. + */ + DEVICE(false, true); + + /** + * Specifies whether it is a room. + */ + private final boolean isRoom; + + /** + * Specifies whether it is physical resource. + */ + private final boolean isPhysical; + + /** + * @param reservationRequestSummary + * @return {@link SpecificationType} from given {@code reservationRequestSummary} + */ + public static SpecificationType fromReservationRequestSummary(ReservationRequestSummary reservationRequestSummary) + { + return fromReservationRequestSummary(reservationRequestSummary, false); + } + + /** + * @param reservationRequestSummary + * @return {@link SpecificationType} from given {@code reservationRequestSummary} + */ + public static SpecificationType fromReservationRequestSummary( + ReservationRequestSummary reservationRequestSummary, + boolean onlyGeneralType) + { + ControllerConfiguration configuration = getControllerConfiguration(); + + switch (reservationRequestSummary.getSpecificationType()) { + case ROOM: + case PERMANENT_ROOM: + return VIRTUAL_ROOM; + case USED_ROOM: + return ROOM_CAPACITY; + case RESOURCE: + if (onlyGeneralType) { + return PHYSICAL_RESOURCE; + } + Set resourceTags = reservationRequestSummary.getResourceTags().stream().map(Tag::getName).collect(Collectors.toSet()); + String parkTagName = configuration.getParkingPlaceTagName(); + String vehicleTagName = configuration.getVehicleTagName(); + String deviceTagName = configuration.getDeviceTagName(); + if (parkTagName != null && resourceTags.contains(parkTagName)) { + return PARKING_PLACE; + } else if (vehicleTagName != null && resourceTags.contains(vehicleTagName)) { + return VEHICLE; + } else if (deviceTagName != null && resourceTags.contains(deviceTagName)) { + return DEVICE; + } + return MEETING_ROOM; + default: + throw new TodoImplementException(reservationRequestSummary.getSpecificationType()); + } + } + + /** + * @param string + * @return {@link SpecificationType} from given {@code string} + */ + public static SpecificationType fromString(String string) + { + ControllerConfiguration configuration = getControllerConfiguration(); + + if (string == null) { + return null; + } + else if (string.equals(configuration.getMeetingRoomTagName())) { + return MEETING_ROOM; + } + else if (string.equals(configuration.getVehicleTagName())) { + return VEHICLE; + } + else if (string.equals(configuration.getParkingPlaceTagName())) { + return PARKING_PLACE; + } else if (string.equals(configuration.getDeviceTagName())) { + return DEVICE; + } + throw new TodoImplementException("SpecificationType.fromString for " + string); + + } + + private static ControllerConfiguration getControllerConfiguration() + { + Controller controller = Controller.getInstance(); + return controller.getConfiguration(); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/VirtualRoomModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/VirtualRoomModel.java new file mode 100644 index 000000000..b65d7e80f --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/reservationrequest/VirtualRoomModel.java @@ -0,0 +1,34 @@ +package cz.cesnet.shongo.controller.rest.models.reservationrequest; + +import cz.cesnet.shongo.Technology; +import cz.cesnet.shongo.controller.api.ExecutableState; +import cz.cesnet.shongo.controller.api.ReservationRequestSummary; +import cz.cesnet.shongo.controller.rest.models.TechnologyModel; +import lombok.Data; + +import java.util.Set; + +/** + * Represents data about virtual room of {@link cz.cesnet.shongo.controller.api.ReservationRequestSummary}. + * + * @author Filip Karnis + */ +@Data +public class VirtualRoomModel +{ + + private String roomName; + private ExecutableState state; + private TechnologyModel technology; + private Boolean hasRoomRecordings; + + public VirtualRoomModel(ReservationRequestSummary summary) + { + this.roomName = summary.getRoomName(); + this.hasRoomRecordings = summary.hasRoomRecordings(); + this.state = summary.getExecutableState(); + + Set technologies = summary.getSpecificationTechnologies(); + this.technology = TechnologyModel.find(technologies); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ReservationModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ReservationModel.java new file mode 100644 index 000000000..9bbd5c7d2 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ReservationModel.java @@ -0,0 +1,33 @@ +package cz.cesnet.shongo.controller.rest.models.resource; + +import cz.cesnet.shongo.api.UserInformation; +import cz.cesnet.shongo.controller.api.ReservationSummary; +import cz.cesnet.shongo.controller.rest.models.TimeInterval; +import lombok.Data; + +/** + * Represents a {@link ReservationSummary}. + * + * @author Filip Karnis + */ +@Data +public class ReservationModel +{ + + private String id; + private TimeInterval slot; + private int licenseCount; + private String requestId; + private UserInformation user; + + public static ReservationModel fromApi(ReservationSummary reservationSummary, UserInformation user) + { + ReservationModel reservationModel = new ReservationModel(); + reservationModel.setId(reservationSummary.getId()); + reservationModel.setSlot(TimeInterval.fromApi(reservationSummary.getSlot())); + reservationModel.setLicenseCount(reservationSummary.getRoomLicenseCount()); + reservationModel.setRequestId(reservationSummary.getReservationRequestId()); + reservationModel.setUser(user); + return reservationModel; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourceCapacity.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourceCapacity.java new file mode 100644 index 000000000..28329ed08 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourceCapacity.java @@ -0,0 +1,132 @@ +package cz.cesnet.shongo.controller.rest.models.resource; + +import cz.cesnet.shongo.controller.api.RecordingCapability; +import cz.cesnet.shongo.controller.api.ReservationSummary; +import cz.cesnet.shongo.controller.api.ResourceSummary; +import cz.cesnet.shongo.controller.api.RoomProviderCapability; + +/** + * Represents a type of capacity which can be utilized in a resource. + *

+ * Theoretically multiple different capacities can be utilized for a single resource + * (e.g., capacity of room licenses and recording capacity) + * + * @author Martin Srom + */ +public abstract class ResourceCapacity +{ + /** + * {@link ResourceSummary} of the resource in which the capacity is utilized. + */ + protected ResourceSummary resource; + + /** + * Constructor. + * + * @param resource sets the {@link #resource} + */ + public ResourceCapacity(ResourceSummary resource) + { + this.resource = resource; + } + + /** + * @return {@link ResourceSummary#getId()} for {@link #resource} + */ + public String getResourceId() + { + return resource.getId(); + } + + /** + * @return {@link ResourceSummary#getName()} for {@link #resource} + */ + public String getResourceName() + { + return resource.getName(); + } + + /** + * @return {@link ReservationSummary.Type} of reservations which allocate this resource capacity + * (e.g., capacity of room licenses are allocated by {@link ReservationSummary.Type#ROOM}) + */ + public abstract ReservationSummary.Type getReservationType(); + + /** + * Abstract {@link ResourceCapacity} for types with license count (e.g., recording capacity or room capacity). + */ + protected static abstract class LicenseCount extends ResourceCapacity + { + /** + * Total number of available licenses in the resource. + */ + protected Integer licenseCount; + + /** + * Constructor. + * + * @param resource sets the {@link #resource} + * @param licenseCount sets the {@link #licenseCount} + */ + public LicenseCount(ResourceSummary resource, Integer licenseCount) + { + super(resource); + + this.licenseCount = licenseCount; + } + + /** + * @return {@link #licenseCount} + */ + public Integer getLicenseCount() + { + return licenseCount; + } + } + + /** + * {@link ResourceCapacity} for resources which provides virtual rooms. + */ + public static class Room extends LicenseCount + { + /** + * Constructor. + * + * @param resource sets the {@link #resource} + * @param capability to be used for determining available license count + */ + public Room(ResourceSummary resource, RoomProviderCapability capability) + { + super(resource, capability.getLicenseCount()); + } + + @Override + public ReservationSummary.Type getReservationType() + { + return ReservationSummary.Type.ROOM; + } + } + + /** + * {@link ResourceCapacity} for resources which provides recording. + */ + public static class Recording extends LicenseCount + { + /** + * Constructor. + * + * @param resource sets the {@link #resource} + * @param capability to be used for determining available license count + */ + public Recording(ResourceSummary resource, RecordingCapability capability) + { + super(resource, capability.getLicenseCount()); + } + + @Override + public ReservationSummary.Type getReservationType() + { + return ReservationSummary.Type.RECORDING_SERVICE; + } + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourceCapacityBucket.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourceCapacityBucket.java new file mode 100644 index 000000000..63cd48957 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourceCapacityBucket.java @@ -0,0 +1,98 @@ +package cz.cesnet.shongo.controller.rest.models.resource; + +import cz.cesnet.shongo.TodoImplementException; +import cz.cesnet.shongo.controller.api.ReservationSummary; +import cz.cesnet.shongo.controller.api.RoomReservation; +import cz.cesnet.shongo.util.RangeSet; +import org.joda.time.DateTime; + +/** + * Represents a special version of {@link RangeSet.Bucket} from which a sum of license count + * for all contained {@link ReservationSummary} can be determined. + * + * @author Martin Srom + */ +public class ResourceCapacityBucket extends RangeSet.Bucket + implements Comparable +{ + /** + * Sum of {@link RoomReservation#getLicenseCount()} + */ + private int licenseCount = 0; + + /** + * Constructor. + * + * @param rangeValue + */ + public ResourceCapacityBucket(DateTime rangeValue) + { + super(rangeValue); + } + + /** + * @return {@link #rangeValue} + */ + public DateTime getDateTime() + { + return getRangeValue(); + } + + /** + * @return {@link #licenseCount} + */ + public int getLicenseCount() + { + return licenseCount; + } + + @Override + public boolean add(ReservationSummary reservation) + { + if (super.add(reservation)) { + switch (reservation.getType()) { + case ROOM: + this.licenseCount += reservation.getRoomLicenseCount(); + break; + case RECORDING_SERVICE: + this.licenseCount++; + break; + default: + throw new TodoImplementException(reservation.getType()); + } + return true; + } + else { + return false; + } + } + + @Override + public boolean remove(Object object) + { + if (super.remove(object)) { + ReservationSummary reservation = (ReservationSummary) object; + switch (reservation.getType()) { + case ROOM: + this.licenseCount -= reservation.getRoomLicenseCount(); + break; + case RECORDING_SERVICE: + this.licenseCount--; + break; + default: + throw new TodoImplementException(reservation.getType()); + } + return true; + } + else { + return false; + } + } + + @Override + public int compareTo(ResourceCapacityBucket bucket) + { + // Bucket with higher utilization should before bucket with lower utilization + return -Double.compare(getLicenseCount(), bucket.getLicenseCount()); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourceCapacityUtilization.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourceCapacityUtilization.java new file mode 100644 index 000000000..51b41855a --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourceCapacityUtilization.java @@ -0,0 +1,108 @@ +package cz.cesnet.shongo.controller.rest.models.resource; + +import cz.cesnet.shongo.controller.api.ReservationSummary; +import org.joda.time.Interval; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * Represents a utilization of {@link ResourceCapacity} for a specific interval. + *

+ * It can be initialized from list of {@link ResourceCapacityBucket}s (which contain all reservations in interval). + * From the buckets we can determine maximum utilization or compute average utilization. + * + * @author Martin Srom + */ +public class ResourceCapacityUtilization +{ + /** + * List of {@link ResourceCapacityBucket}s. Each bucket represents an interval in which {@link ResourceCapacity} is + * utilized in some way. Different buckets means different utilization. Buckets shall be sorted to be able to + * determine maximum utilization. + */ + private final List buckets = new LinkedList<>(); + + /** + * List of {@link ReservationSummary} in all {@link #buckets}. + */ + private List reservations; + + /** + * Constructor. + * + * @param buckets sets the {@link #buckets} + */ + public ResourceCapacityUtilization(Collection buckets) + { + this.buckets.addAll(buckets); + + // Sort buckets (to be able to determine maximum utilization) + Collections.sort(this.buckets); + } + + /** + * @return first {@link ResourceCapacityBucket} from {@link #buckets} with maximum utilization. + */ + public ResourceCapacityBucket getPeakBucket() + { + if (buckets.size() > 0) { + return buckets.get(0); + } + else { + return null; + } + } + + /** + * @return {@link #buckets} + */ + public List getBuckets() + { + return Collections.unmodifiableList(buckets); + } + + /** + * @return {@link #reservations} + */ + public Collection getReservations() + { + if (this.reservations == null) { + Set reservations = new LinkedHashSet(); + for (ResourceCapacityBucket bucket : buckets) { + reservations.addAll(bucket); + } + this.reservations = new LinkedList<>(); + this.reservations.addAll(reservations); + this.reservations.sort((reservation1, reservation2) -> { + Interval reservationSlot1 = reservation1.getSlot(); + Interval reservationSlot2 = reservation2.getSlot(); + + int result = reservationSlot1.getStart().compareTo(reservationSlot2.getStart()); + if (result != 0) { + return result; + } + + return reservationSlot1.getEnd().compareTo(reservationSlot2.getEnd()); + }); + } + return Collections.unmodifiableList(this.reservations); + } + + /** + * @return user-ids of {@link #reservations} + */ + public Collection getReservationUserIds() + { + Set userIds = new HashSet<>(); + for (ReservationSummary reservation : getReservations()) { + userIds.add(reservation.getUserId()); + } + return userIds; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourceModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourceModel.java new file mode 100644 index 000000000..d0d4354c9 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourceModel.java @@ -0,0 +1,37 @@ +package cz.cesnet.shongo.controller.rest.models.resource; + +import cz.cesnet.shongo.controller.api.ResourceSummary; +import cz.cesnet.shongo.controller.api.Tag; +import cz.cesnet.shongo.controller.rest.models.TechnologyModel; +import lombok.Data; + +import java.util.Set; + +/** + * Represents {@link ResourceSummary}. + * + * @author Filip Karnis + */ +@Data +public class ResourceModel +{ + + private String id; + private ResourceSummary.Type type; + private String name; + private String description; + private TechnologyModel technology; + private Set tags; + private boolean hasCapacity; + + public ResourceModel(ResourceSummary summary) + { + this.id = summary.getId(); + this.type = summary.getType(); + this.name = summary.getName(); + this.description = summary.getDescription(); + this.technology = TechnologyModel.find(summary.getTechnologies()); + this.tags = summary.getTags(); + this.hasCapacity = summary.hasCapacity(); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourceUtilizationDetailModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourceUtilizationDetailModel.java new file mode 100644 index 000000000..c850693bd --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourceUtilizationDetailModel.java @@ -0,0 +1,39 @@ +package cz.cesnet.shongo.controller.rest.models.resource; + +import lombok.Data; + +import java.util.List; + +/** + * Represents a utilization of {@link ResourceCapacity} in a specific interval. + * + * @author Filip Karnis + */ +@Data +public class ResourceUtilizationDetailModel +{ + + private String id; + private String name; + private int totalCapacity; + private int usedCapacity; + private List reservations; + + public static ResourceUtilizationDetailModel fromApi( + ResourceCapacityUtilization resourceCapacityUtilization, + ResourceCapacity.Room roomCapacity, + List reservations) + { + int licenseCount = (resourceCapacityUtilization != null) + ? resourceCapacityUtilization.getPeakBucket().getLicenseCount() + : 0; + + ResourceUtilizationDetailModel resourceUtilizationDetailModel = new ResourceUtilizationDetailModel(); + resourceUtilizationDetailModel.setId(roomCapacity.getResourceId()); + resourceUtilizationDetailModel.setName(roomCapacity.getResourceName()); + resourceUtilizationDetailModel.setTotalCapacity(roomCapacity.getLicenseCount()); + resourceUtilizationDetailModel.setUsedCapacity(licenseCount); + resourceUtilizationDetailModel.setReservations(reservations); + return resourceUtilizationDetailModel; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourceUtilizationModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourceUtilizationModel.java new file mode 100644 index 000000000..5a693260a --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourceUtilizationModel.java @@ -0,0 +1,59 @@ +package cz.cesnet.shongo.controller.rest.models.resource; + +import cz.cesnet.shongo.controller.api.ReservationSummary; +import cz.cesnet.shongo.controller.rest.models.TimeInterval; +import lombok.Data; +import org.joda.time.Interval; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Represents a utilization of all {@link ResourceCapacity} in a specific interval. + * + * @author Filip Karnis + */ +@Data +public class ResourceUtilizationModel +{ + + private TimeInterval interval; + private List resources; + + public static ResourceUtilizationModel fromApi( + Interval interval, + Map resourceCapacityUtilizations) + { + List resources = new ArrayList<>(); + resourceCapacityUtilizations.forEach((resourceCapacity, resourceCapacityUtilization) -> { + UtilizationModel utilizationModel = new UtilizationModel(); + utilizationModel.setId(resourceCapacity.getResourceId()); + utilizationModel.setName(resourceCapacity.getResourceName()); + if (resourceCapacity instanceof ResourceCapacity.LicenseCount) { + ResourceCapacity.LicenseCount licenseCount = (ResourceCapacity.LicenseCount) resourceCapacity; + utilizationModel.setTotalCapacity(licenseCount.getLicenseCount()); + } + utilizationModel.setUsedCapacity((resourceCapacityUtilization != null) + ? resourceCapacityUtilization.getPeakBucket().getLicenseCount() : 0); + utilizationModel.setType(resourceCapacity.getReservationType()); + resources.add(utilizationModel); + }); + + ResourceUtilizationModel resourceUtilizationModel = new ResourceUtilizationModel(); + resourceUtilizationModel.setInterval(TimeInterval.fromApi(interval)); + resourceUtilizationModel.setResources(resources); + return resourceUtilizationModel; + } + + @Data + public static class UtilizationModel + { + + private String id; + private String name; + private int totalCapacity; + private int usedCapacity; + private ReservationSummary.Type type; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourcesUtilization.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourcesUtilization.java new file mode 100644 index 000000000..78fe8c1f4 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/ResourcesUtilization.java @@ -0,0 +1,386 @@ +package cz.cesnet.shongo.controller.rest.models.resource; + +import cz.cesnet.shongo.TodoImplementException; +import cz.cesnet.shongo.controller.api.Capability; +import cz.cesnet.shongo.controller.api.RecordingCapability; +import cz.cesnet.shongo.controller.api.ReservationSummary; +import cz.cesnet.shongo.controller.api.Resource; +import cz.cesnet.shongo.controller.api.ResourceSummary; +import cz.cesnet.shongo.controller.api.RoomProviderCapability; +import cz.cesnet.shongo.controller.api.SecurityToken; +import cz.cesnet.shongo.controller.api.request.ReservationListRequest; +import cz.cesnet.shongo.controller.api.request.ResourceListRequest; +import cz.cesnet.shongo.controller.api.rpc.ReservationService; +import cz.cesnet.shongo.controller.api.rpc.ResourceService; +import cz.cesnet.shongo.controller.rest.RestCache; +import cz.cesnet.shongo.util.RangeSet; +import lombok.extern.slf4j.Slf4j; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * Represents utilization of all types of capacities for all resources to which a single user has access. + * + * @author Martin Srom + */ +@Slf4j +public class ResourcesUtilization +{ + + /** + * {@link SecurityToken} of user to which the {@link ResourcesUtilization} belongs. + */ + private final SecurityToken securityToken; + + /** + * {@link ReservationService} for retrieving reservations. + */ + private final ReservationService reservationService; + + /** + * List of {@link ResourceCapacity} to which the user has access. + */ + private final List resourceCapacities = new LinkedList(); + + /** + * Map of {@link ResourceCapacity} by class and by resource-id. + */ + private final Map, ResourceCapacity>> resourceCapacityMap = + new HashMap<>(); + + /** + * Map of cached {@link ResourceCapacityUtilization} for {@link ResourceCapacity} and for {@link Interval}. + */ + private final Map> resourceCapacityUtilizationMap = + new HashMap<>(); + + /** + * Map of cached {@link ReservationSummary}s for {@link ResourceCapacity}. + */ + private final Map> reservationSetMap = + new HashMap<>(); + + /** + * {@link Interval} which is already cached in {@link #reservationSetMap}. + */ + private Interval reservationInterval; + + /** + * Constructor. + * + * @param securityToken sets the {@link #securityToken} + * @param reservations sets the {@link #reservationService} + * @param resources to be used for fetching resources + * @param cache to be used for fetching {@link #resourceCapacities} + */ + public ResourcesUtilization( + SecurityToken securityToken, + ReservationService reservations, + ResourceService resources, + RestCache cache) + { + this.securityToken = securityToken; + this.reservationService = reservations; + + // Fetch ResourceCapacities for all accessible resources + ResourceListRequest resourceListRequest = new ResourceListRequest(securityToken); + resourceListRequest.setSort(ResourceListRequest.Sort.NAME); + resourceListRequest.addCapabilityClass(RoomProviderCapability.class); + resourceListRequest.addCapabilityClass(RecordingCapability.class); + for (ResourceSummary resourceSummary : resources.listResources(resourceListRequest)) { + String resourceId = resourceSummary.getId(); + Resource resource = cache.getResource(securityToken, resourceId); + for (Capability capability : resource.getCapabilities()) { + if (capability instanceof RoomProviderCapability) { + RoomProviderCapability roomProviderCapability = (RoomProviderCapability) capability; + addResourceCapacity(new ResourceCapacity.Room(resourceSummary, roomProviderCapability)); + } + else if (capability instanceof RecordingCapability) { + RecordingCapability recordingCapability = (RecordingCapability) capability; + if (recordingCapability.getLicenseCount() != null) { + addResourceCapacity(new ResourceCapacity.Recording(resourceSummary, recordingCapability)); + } + } + } + } + } + + /** + * @return {@link #resourceCapacities} + */ + public Collection getResourceCapacities() + { + return Collections.unmodifiableList(resourceCapacities); + } + + /** + * @param resourceId + * @param capacityClass + * @return {@link ResourceCapacity} for given {@code resourceId} and {@code capacityClass} + */ + public ResourceCapacity getResourceCapacity(String resourceId, Class capacityClass) + { + Map, ResourceCapacity> resourceCapacitiesByClass = + resourceCapacityMap.get(resourceId); + if (resourceCapacitiesByClass == null) { + return null; + } + return resourceCapacitiesByClass.get(capacityClass); + } + + /** + * @param interval interval to be returned + * @param period by which the {@code interval} should be split and for each part should be {@link ResourceCapacityUtilization} computed + * @return map of {@link ResourceCapacityUtilization} by {@link ResourceCapacity}s and by {@link Interval}s + */ + public Map> getUtilization( + Interval interval, + Period period) + { + Map> utilizationsByInterval = + new LinkedHashMap<>(); + DateTime start = interval.getStart(); + DateTime maxEnd = interval.getEnd(); + while (start.isBefore(maxEnd)) { + DateTime end = start.plus(period); + if (end.isAfter(maxEnd)) { + end = maxEnd; + } + Interval utilizationInterval = new Interval(start, end); + Map utilizations = + new HashMap(); + for (ResourceCapacity resourceCapacity : resourceCapacities) { + ResourceCapacityUtilization utilization = + getUtilization(resourceCapacity, utilizationInterval, true, interval); + utilizations.put(resourceCapacity, utilization); + } + utilizationsByInterval.put(utilizationInterval, utilizations); + start = end; + } + return utilizationsByInterval; + } + + /** + * @param resourceCapacity + * @param interval + * @return {@link ResourceCapacityUtilization} for given {@code resourceCapacity} and {@code interval} + */ + public ResourceCapacityUtilization getUtilization(ResourceCapacity resourceCapacity, Interval interval) + { + return getUtilization(resourceCapacity, interval, false, interval); + } + + /** + * @param resourceCapacity to be added to the {@link #resourceCapacities} and {@link #resourceCapacityMap} + */ + private void addResourceCapacity(ResourceCapacity resourceCapacity) + { + if (resourceCapacities.add(resourceCapacity)) { + String resourceId = resourceCapacity.getResourceId(); + Class resourceCapacityClass = resourceCapacity.getClass(); + + Map, ResourceCapacity> resourceCapacitiesByClass = + resourceCapacityMap.get(resourceId); + if (resourceCapacitiesByClass == null) { + resourceCapacitiesByClass = new HashMap<>(); + resourceCapacityMap.put(resourceId, resourceCapacitiesByClass); + } + resourceCapacitiesByClass.put(resourceCapacityClass, resourceCapacity); + } + } + + /** + * Can be used for bulk fetching of {@link ReservationSummary}s by use {@code fetchAll} and {@code fetchInterval}. + * + * @param resourceCapacity + * @param interval + * @param fetchAll + * @param fetchInterval + * @return {@link ResourceCapacityUtilization} for given {@code resourceCapacity} and {@code interval} + */ + private ResourceCapacityUtilization getUtilization( + ResourceCapacity resourceCapacity, + Interval interval, + boolean fetchAll, Interval fetchInterval) + { + // Try to return cached utilization + Map utilizations = resourceCapacityUtilizationMap.get(interval); + if (utilizations == null) { + utilizations = new HashMap<>(); + resourceCapacityUtilizationMap.put(interval, utilizations); + } + if (utilizations.containsKey(resourceCapacity)) { + return utilizations.get(resourceCapacity); + } + + // Fetch reservations + RangeSet reservations; + if (fetchAll) { + reservations = getReservationsWithFetchAll(resourceCapacity, fetchInterval); + } + else { + reservations = getReservations(resourceCapacity, fetchInterval); + } + + // Prepare new utilization + ResourceCapacityUtilization utilization = null; + if (reservations != null) { + Collection buckets = + reservations.getBuckets(interval.getStart(), interval.getEnd(), ResourceCapacityBucket.class); + if (buckets.size() > 0) { + utilization = new ResourceCapacityUtilization(buckets); + } + } + + // Store the utilization to cache and return it + utilizations.put(resourceCapacity, utilization); + return utilization; + } + + /** + * Get {@link RangeSet} for given {@code resourceCapacity} + * by fetching {@link ReservationSummary}s for all {@link #resourceCapacities}. + *

+ * The newly fetched {@link ReservationSummary}s will stored in {@link #reservationSetMap} + * (because all {@link ResourceCapacity}s will be updated). + * + * @param resourceCapacity + * @param interval + * @return {@link RangeSet} of {@link ReservationSummary} + */ + private synchronized RangeSet getReservationsWithFetchAll( + ResourceCapacity resourceCapacity, Interval interval) + { + // Try to return cached reservations + if (this.reservationInterval != null && this.reservationInterval.contains(interval)) { + return this.reservationSetMap.get(resourceCapacity); + } + DateTime start = interval.getStart(); + DateTime end = interval.getEnd(); + + // Expand reservation cache at start + if (reservationInterval != null && + reservationInterval.isAfter(start) && reservationInterval.contains(end)) { + interval = new Interval(start, reservationInterval.getStart()); + reservationInterval = new Interval(start, reservationInterval.getEnd()); + } + // Expand reservation cache at end + else if (reservationInterval != null && + reservationInterval.isBefore(end) && reservationInterval.contains(start)) { + interval = new Interval(reservationInterval.getEnd(), end); + reservationInterval = new Interval(reservationInterval.getStart(), end); + } + // Load the whole reservation cache + else { + log.info("Clearing cached reservations..."); + reservationSetMap.clear(); + reservationInterval = interval; + } + + // Fetch reservations for all resource capacities + log.info("Loading reservations for {}...", interval); + ReservationListRequest reservationListRequest = new ReservationListRequest(securityToken); + for (ResourceCapacity currentResourceCapacity : resourceCapacities) { + reservationListRequest.addResourceId(currentResourceCapacity.getResourceId()); + } + reservationListRequest.addReservationType(ReservationSummary.Type.ROOM); + reservationListRequest.addReservationType(ReservationSummary.Type.RECORDING_SERVICE); + reservationListRequest.setInterval(interval); + for (ReservationSummary reservation : reservationService.listReservations(reservationListRequest)) { + Interval reservationSlot = reservation.getSlot(); + String reservationResourceId = reservation.getResourceId(); + ResourceCapacity reservationResourceCapacity = getResourceCapacity(reservationResourceId, reservation); + RangeSet reservationSet = reservationSetMap.get(reservationResourceCapacity); + if (reservationSet == null) { + reservationSet = new RangeSet<>() + { + @Override + protected Bucket createBucket(DateTime rangeValue) + { + return new ResourceCapacityBucket(rangeValue); + } + }; + reservationSetMap.put(reservationResourceCapacity, reservationSet); + } + reservationSet.add(reservation, reservationSlot.getStart(), reservationSlot.getEnd()); + } + return reservationSetMap.get(resourceCapacity); + } + + /** + * Get {@link RangeSet} for given {@code resourceCapacity} + * by fetching {@link ReservationSummary}s only for given {@code resourceCapacity}. + *

+ * The newly fetched {@link ReservationSummary}s won't be stored in {@link #reservationSetMap} + * (because all {@link ResourceCapacity} won't be updated). + * + * @param resourceCapacity + * @param interval + * @return {@link RangeSet} of {@link ReservationSummary} + */ + private RangeSet getReservations( + ResourceCapacity resourceCapacity, + Interval interval) + { + // Try to return cached reservations + synchronized (this) { + if (this.reservationInterval != null && this.reservationInterval.contains(interval)) { + return this.reservationSetMap.get(resourceCapacity); + } + } + + // Fetch reservations for single resource capacity + RangeSet reservationSet = new RangeSet<>() + { + @Override + protected Bucket createBucket(DateTime rangeValue) + { + return new ResourceCapacityBucket(rangeValue); + } + }; + ReservationListRequest reservationListRequest = new ReservationListRequest(securityToken); + reservationListRequest.addResourceId(resourceCapacity.getResourceId()); + reservationListRequest.addReservationType(resourceCapacity.getReservationType()); + reservationListRequest.setInterval(interval); + for (ReservationSummary reservation : reservationService.listReservations(reservationListRequest)) { + Interval reservationSlot = reservation.getSlot(); + reservationSet.add(reservation, reservationSlot.getStart(), reservationSlot.getEnd()); + } + return reservationSet; + } + + /** + * @param resourceId + * @param reservation + * @return {@link ResourceCapacity} for given {@code resourceId} and {@code reservation} + */ + private ResourceCapacity getResourceCapacity(String resourceId, ReservationSummary reservation) + { + return getResourceCapacity(resourceId, getResourceCapacityClass(reservation.getType())); + } + + /** + * @param reservationType + * @return class of {@link ResourceCapacity} for given {@code reservationType} + */ + private Class getResourceCapacityClass(ReservationSummary.Type reservationType) + { + switch (reservationType) { + case ROOM: + return ResourceCapacity.Room.class; + case RECORDING_SERVICE: + return ResourceCapacity.Recording.class; + default: + throw new TodoImplementException(reservationType); + } + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/Unit.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/Unit.java new file mode 100644 index 000000000..2393acc12 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/resource/Unit.java @@ -0,0 +1,23 @@ +package cz.cesnet.shongo.controller.rest.models.resource; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.joda.time.Period; + +/** + * Represents a unit of time to use as a period for utilization of {@link ResourceCapacity}. + * + * @author Filip Karnis + */ +@Getter +@AllArgsConstructor +public enum Unit +{ + + DAY(Period.days(1)), + WEEK(Period.weeks(1)), + MONTH(Period.months(1)), + YEAR(Period.years(1)); + + private final Period period; +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/roles/UserRoleModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/roles/UserRoleModel.java new file mode 100644 index 000000000..4f5bdfc4c --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/roles/UserRoleModel.java @@ -0,0 +1,176 @@ +package cz.cesnet.shongo.controller.rest.models.roles; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import cz.cesnet.shongo.TodoImplementException; +import cz.cesnet.shongo.api.UserInformation; +import cz.cesnet.shongo.controller.AclIdentityType; +import cz.cesnet.shongo.controller.ObjectRole; +import cz.cesnet.shongo.controller.api.AclEntry; +import cz.cesnet.shongo.controller.api.Group; +import cz.cesnet.shongo.controller.rest.CacheProvider; +import cz.cesnet.shongo.controller.rest.models.CommonModel; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents entity's role for specified resource. + * + * @author Martin Srom + * @author Filip Karnis + */ +@Data +@NoArgsConstructor +public class UserRoleModel +{ + private String id; + + @JsonProperty("type") + private AclIdentityType identityType; + + @JsonProperty("entityId") + private String identityPrincipalId; + + private String identityName; + + private String identityDescription; + + private String email; + + private String objectId; + + private ObjectRole role; + + private boolean deletable = false; + + @JsonIgnore + private CacheProvider cacheProvider; + + @JsonIgnore + private UserInformation user; + + @JsonIgnore + private Group group; + + public UserRoleModel(UserInformation userInformation) + { + this.identityType = AclIdentityType.USER; + this.identityPrincipalId = userInformation.getUserId(); + this.user = userInformation; + } + + public UserRoleModel(CacheProvider cacheProvider, AclIdentityType type) + { + this.cacheProvider = cacheProvider; + this.identityType = type; + } + + public UserRoleModel(AclEntry aclEntry, CacheProvider cacheProvider) + { + this.cacheProvider = cacheProvider; + fromApi(aclEntry); + } + + private boolean isNew() + { + return id == null || CommonModel.isNewId(id); + } + + public void setNewId() + { + this.id = CommonModel.getNewId(); + } + + public void setIdentityType(AclIdentityType identityType) + { + if (!identityType.equals(this.identityType)) { + user = null; + group = null; + } + this.identityType = identityType; + } + + public void setIdentityPrincipalId(String identityPrincipalId) + { + if (!identityPrincipalId.equals(this.identityPrincipalId)) { + user = null; + group = null; + } + this.identityPrincipalId = identityPrincipalId; + } + + private String getIdentityNameFromApi() + { + switch (identityType) { + case USER: + return getUser().getFullName(); + case GROUP: + return getGroup().getName(); + default: + throw new TodoImplementException(identityType); + } + } + + private String getIdentityDescriptionFromApi() + { + switch (identityType) { + case USER: + return getUser().getOrganization(); + case GROUP: + return getGroup().getDescription(); + default: + throw new TodoImplementException(identityType); + } + } + + @JsonIgnore + public UserInformation getUser() + { + if (user == null && identityType.equals(AclIdentityType.USER)) { + if (cacheProvider == null) { + throw new IllegalStateException("CacheProvider isn't set."); + } + user = cacheProvider.getUserInformation(identityPrincipalId); + } + return user; + } + + private Group getGroup() + { + if (group == null && identityType.equals(AclIdentityType.GROUP)) { + if (cacheProvider == null) { + throw new IllegalStateException("CacheProvider isn't set."); + } + group = cacheProvider.getGroup(identityPrincipalId); + } + return group; + } + + public void fromApi(AclEntry aclEntry) + { + this.id = aclEntry.getId(); + setIdentityType(aclEntry.getIdentityType()); + setIdentityPrincipalId(aclEntry.getIdentityPrincipalId()); + setIdentityName(getIdentityNameFromApi()); + setIdentityDescription(getIdentityDescriptionFromApi()); + if (AclIdentityType.USER.equals(identityType)) { + setEmail(getUser().getEmail()); + } + setObjectId(aclEntry.getObjectId()); + setRole(aclEntry.getRole()); + setDeletable(aclEntry.isDeletable()); + } + + public AclEntry toApi() + { + AclEntry aclEntry = new AclEntry(); + if (!isNew()) { + aclEntry.setId(id); + } + aclEntry.setIdentityType(identityType); + aclEntry.setIdentityPrincipalId(identityPrincipalId); + aclEntry.setObjectId(objectId); + aclEntry.setRole(role); + return aclEntry; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/room/RoomAuthorizedData.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/room/RoomAuthorizedData.java new file mode 100644 index 000000000..01a2af7f1 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/room/RoomAuthorizedData.java @@ -0,0 +1,32 @@ +package cz.cesnet.shongo.controller.rest.models.room; + +import cz.cesnet.shongo.api.Alias; +import cz.cesnet.shongo.controller.api.AbstractRoomExecutable; +import lombok.Data; + +import java.util.List; + +/** + * Represents authorized data for {@link AbstractRoomExecutable}. + * + * @author Filip Karnis + */ +@Data +public class RoomAuthorizedData +{ + + private String pin; + private String adminPin; + private String guestPin; + private Boolean allowGuests; + private List aliases; + + public RoomAuthorizedData(AbstractRoomExecutable roomExecutable) + { + pin = roomExecutable.getPin(); + adminPin = roomExecutable.getAdminPin(); + guestPin = roomExecutable.getGuestPin(); + allowGuests = roomExecutable.getAllowGuests(); + aliases = roomExecutable.getAliases(); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/room/RoomModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/room/RoomModel.java new file mode 100644 index 000000000..e5801cd3e --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/room/RoomModel.java @@ -0,0 +1,67 @@ +package cz.cesnet.shongo.controller.rest.models.room; + +import cz.cesnet.shongo.controller.api.ExecutableSummary; +import cz.cesnet.shongo.controller.rest.models.TechnologyModel; +import cz.cesnet.shongo.controller.rest.models.TimeInterval; +import lombok.Data; +import org.joda.time.Interval; + +/** + * Represents {@link ExecutableSummary}. + * + * @author Filip Karnis + */ +@Data +public class RoomModel +{ + + private String id; + private ExecutableSummary.Type type; + private TimeInterval slot; + private TimeInterval earliestSlot; + private String description; + private String name; + private TechnologyModel technology; + private RoomState state; + private boolean isDeprecated; + private int licenceCount; + private int usageCount; + + public RoomModel(ExecutableSummary summary) + { + this.id = summary.getId(); + this.type = summary.getType(); + this.name = summary.getRoomName(); + this.description = summary.getRoomDescription(); + this.technology = TechnologyModel.find(summary.getRoomTechnologies()); + this.usageCount = summary.getRoomUsageCount(); + + this.state = RoomState.fromRoomState( + summary.getState(), summary.getRoomLicenseCount(), + summary.getRoomUsageState()); + + Interval slot = summary.getRoomUsageSlot(); + if (slot == null) { + slot = summary.getSlot(); + } + this.slot = TimeInterval.fromApi(slot); + + boolean isDeprecated; + switch (state) { + case STARTED: + case STARTED_AVAILABLE: + isDeprecated = false; + break; + default: + isDeprecated = slot.getEnd().isBeforeNow(); + break; + } + this.isDeprecated = isDeprecated; + + Integer licenseCount = summary.getRoomUsageLicenseCount(); + if (licenseCount == null) { + licenseCount = summary.getRoomLicenseCount(); + } + this.licenceCount = licenseCount; + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/room/RoomState.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/room/RoomState.java new file mode 100644 index 000000000..1a5361ef1 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/room/RoomState.java @@ -0,0 +1,103 @@ +package cz.cesnet.shongo.controller.rest.models.room; + +import cz.cesnet.shongo.TodoImplementException; +import cz.cesnet.shongo.controller.api.ExecutableState; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Represents a room state. + * + * @author Martin Srom + * @author Filip Karnis + */ +@Getter +@AllArgsConstructor +public enum RoomState +{ + /** + * Room is not started. + */ + NOT_STARTED(false, false), + + /** + * Room is started. + */ + STARTED(true, true), + + /** + * Room is not available for participants to join. + */ + STARTED_NOT_AVAILABLE(true, false), + + /** + * Room is available for participants to join. + */ + STARTED_AVAILABLE(true, true), + + /** + * Room has been stopped. + */ + STOPPED(false, false), + + /** + * Room is not available for participants to join due to error. + */ + FAILED(false, false); + + /** + * Specifies whether this state represents an started room. + */ + private final boolean isStarted; + + /** + * Specifies whether this state represents an available for participants to join room. + */ + private final boolean isAvailable; + + /** + * @param roomState + * @param roomLicenseCount + * @param roomUsageState + * @return {@link RoomState} + */ + public static RoomState fromRoomState( + ExecutableState roomState, + Integer roomLicenseCount, + ExecutableState roomUsageState) + { + switch (roomState) { + case NOT_STARTED: + return NOT_STARTED; + case STARTED: + if (roomUsageState != null) { + // Permanent room with earliest usage + return roomUsageState.isAvailable() ? STARTED_AVAILABLE : STARTED_NOT_AVAILABLE; + } + else if (roomLicenseCount == null || roomLicenseCount == 0) { + // Permanent room without earliest usage + return STARTED_NOT_AVAILABLE; + } + else { + // Other room + return STARTED; + } + case STOPPED: + case STOPPING_FAILED: + return RoomState.STOPPED; + case STARTING_FAILED: + return RoomState.FAILED; + default: + throw new TodoImplementException(roomState); + } + } + + /** + * @param roomState + * @return {@link RoomState} + */ + public static RoomState fromRoomState(ExecutableState roomState) + { + return fromRoomState(roomState, 1, null); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/room/RoomType.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/room/RoomType.java new file mode 100644 index 000000000..b139b1625 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/room/RoomType.java @@ -0,0 +1,60 @@ +package cz.cesnet.shongo.controller.rest.models.room; + +import cz.cesnet.shongo.TodoImplementException; +import cz.cesnet.shongo.controller.api.AbstractRoomExecutable; +import cz.cesnet.shongo.controller.api.ExecutableSummary; +import cz.cesnet.shongo.controller.api.RoomExecutable; +import cz.cesnet.shongo.controller.api.UsedRoomExecutable; + +/** + * Type of {@link RoomModel}. + * + * @author Martin Srom + */ +public enum RoomType +{ + + /** + * Permanent room. + */ + VIRTUAL_ROOM, + + /** + * Used room. + */ + USED_ROOM; + + /** + * @param executableSummary + * @return {@link RoomType} from given {@code executableSummary} + */ + public static RoomType fromExecutableSummary(ExecutableSummary executableSummary) + { + if (executableSummary.getType().equals(ExecutableSummary.Type.ROOM)) { + return VIRTUAL_ROOM; + } + else if (executableSummary.getType().equals(ExecutableSummary.Type.USED_ROOM)) { + return USED_ROOM; + } + else { + throw new TodoImplementException(executableSummary.getType()); + } + } + + /** + * @param roomExecutable + * @return {@link RoomType} from given {@code roomExecutable} + */ + public static RoomType fromRoomExecutable(AbstractRoomExecutable roomExecutable) + { + if (roomExecutable instanceof RoomExecutable) { + return VIRTUAL_ROOM; + } + else if (roomExecutable instanceof UsedRoomExecutable) { + return USED_ROOM; + } + else { + throw new TodoImplementException(roomExecutable.getClass()); + } + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/runtimemanagement/RuntimeParticipantModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/runtimemanagement/RuntimeParticipantModel.java new file mode 100644 index 000000000..e49bad420 --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/runtimemanagement/RuntimeParticipantModel.java @@ -0,0 +1,53 @@ +package cz.cesnet.shongo.controller.rest.models.runtimemanagement; + +import cz.cesnet.shongo.ParticipantRole; +import cz.cesnet.shongo.api.Alias; +import cz.cesnet.shongo.api.RoomLayout; +import cz.cesnet.shongo.api.RoomParticipant; +import cz.cesnet.shongo.api.UserInformation; +import cz.cesnet.shongo.controller.rest.CacheProvider; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a {@link RoomParticipant}. + * + * @author Filip Karnis + */ +@Data +@NoArgsConstructor +public class RuntimeParticipantModel +{ + + private String id; + private String name; + private String email; + private String alias; + private ParticipantRole role; + private RoomLayout layout; + private Boolean microphoneEnabled; + private Integer microphoneLevel; + private Boolean videoEnabled; + private Boolean videoSnapshot; + + public RuntimeParticipantModel(RoomParticipant roomParticipant, CacheProvider cacheProvider) + { + UserInformation user = null; + String userId = roomParticipant.getUserId(); + if (userId != null) { + user = cacheProvider.getUserInformation(userId); + } + + this.id = roomParticipant.getId(); + this.name = (user != null ? user.getFullName() : roomParticipant.getDisplayName()); + this.email = (user != null ? user.getPrimaryEmail() : null); + Alias alias = roomParticipant.getAlias(); + this.alias = (alias != null ? alias.getValue() : null); + this.role = roomParticipant.getRole(); + this.layout = roomParticipant.getLayout(); + this.microphoneEnabled = roomParticipant.getMicrophoneEnabled(); + this.microphoneLevel = roomParticipant.getMicrophoneLevel(); + this.videoEnabled = roomParticipant.getVideoEnabled(); + this.videoSnapshot = roomParticipant.isVideoSnapshot(); + } +} diff --git a/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/users/SettingsModel.java b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/users/SettingsModel.java new file mode 100644 index 000000000..f78e7415a --- /dev/null +++ b/shongo-controller/src/main/java/cz/cesnet/shongo/controller/rest/models/users/SettingsModel.java @@ -0,0 +1,38 @@ +package cz.cesnet.shongo.controller.rest.models.users; + +import cz.cesnet.shongo.controller.SystemPermission; +import cz.cesnet.shongo.controller.api.UserSettings; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.joda.time.DateTimeZone; + +import java.util.List; +import java.util.Locale; + +/** + * Represents {@link UserSettings}. + * + * @author Filip Karnis + */ +@Data +@NoArgsConstructor +public class SettingsModel +{ + + private Boolean useWebService; + private Locale locale; + private DateTimeZone homeTimeZone; + private DateTimeZone currentTimeZone; + private Boolean administrationMode; + private List permissions; + + public SettingsModel(UserSettings userSettings, List permissions) + { + this.useWebService = userSettings.isUseWebService(); + this.locale = userSettings.getLocale(); + this.homeTimeZone = userSettings.getHomeTimeZone(); + this.currentTimeZone = userSettings.getCurrentTimeZone(); + this.administrationMode = userSettings.getAdministrationMode(); + this.permissions = permissions; + } +} 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/WEB-INF/interDomain-servlet.xml b/shongo-controller/src/main/resources/WEB-INF/interDomain-servlet.xml deleted file mode 100644 index 0bc324e63..000000000 --- a/shongo-controller/src/main/resources/WEB-INF/interDomain-servlet.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/shongo-controller/src/main/resources/controller-default.cfg.xml b/shongo-controller/src/main/resources/controller-default.cfg.xml index 3128939ea..a97db9787 100644 --- a/shongo-controller/src/main/resources/controller-default.cfg.xml +++ b/shongo-controller/src/main/resources/controller-default.cfg.xml @@ -62,6 +62,15 @@ PT33S + + 127.0.0.1 + 8001 + http://localhost + https://localhost + + + + diff --git a/shongo-controller/src/main/resources/sql/hsqldb/init.sql b/shongo-controller/src/main/resources/sql/hsqldb/init.sql index 861e651c5..644ed9010 100644 --- a/shongo-controller/src/main/resources/sql/hsqldb/init.sql +++ b/shongo-controller/src/main/resources/sql/hsqldb/init.sql @@ -30,12 +30,14 @@ 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, + MAX(CASE WHEN capability.resource_id IS NOT NULL THEN TRUE ELSE FALSE END) AS has_capacity 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 LEFT JOIN resource_tag ON resource.id = resource_tag.resource_id LEFT JOIN tag ON resource_tag.tag_id = tag.id +LEFT JOIN capability ON capability.resource_id = resource.id GROUP BY resource.id; /** @@ -97,6 +99,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..d1a5f9589 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,12 +113,14 @@ 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, + bool_or(capability.resource_id IS NOT NULL) AS has_capacity 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 LEFT JOIN resource_tag ON resource.id = resource_tag.resource_id LEFT JOIN tag ON resource_tag.tag_id = tag.id +LEFT JOIN capability ON capability.resource_id = resource.id GROUP BY resource.id; /** @@ -340,6 +343,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 +544,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..44d8c0219 100644 --- a/shongo-controller/src/main/resources/sql/reservation_request_list.sql +++ b/shongo-controller/src/main/resources/sql/reservation_request_list.sql @@ -26,10 +26,7 @@ SELECT specification_summary.room_participant_count AS room_participant_count, executable_summary.room_has_recording_service AS room_has_recording_service, executable_summary.room_has_recordings AS room_has_recordings, - CASE - WHEN specification_summary.alias_room_name IS NOT NULL THEN specification_summary.alias_room_name - ELSE reused_specification_summary.alias_room_name - END AS alias_room_name, + executable_summary.room_name AS alias_room_name, specification_summary.resource_id resource_id, reservation_request_summary.usage_executable_state AS usage_executable_state, reservation_request_summary.future_child_count, @@ -37,7 +34,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/main/resources/sql/resource_list.sql b/shongo-controller/src/main/resources/sql/resource_list.sql index 3144861ac..08677e3f3 100644 --- a/shongo-controller/src/main/resources/sql/resource_list.sql +++ b/shongo-controller/src/main/resources/sql/resource_list.sql @@ -2,6 +2,7 @@ * Select query for list of resources. * * @author Martin Srom + * @author Filip Karnis */ SELECT resource_summary.id AS id, @@ -14,7 +15,10 @@ SELECT resource_summary.description AS description, resource_summary.calendar_public AS calendar_public, resource_summary.calendar_uri_key AS calendar_uri_key, - resource_summary.confirm_by_owner AS confirm_by_owner + resource_summary.confirm_by_owner AS confirm_by_owner, + resource_summary.type AS type, + resource_summary.tags AS tags, + resource_summary.has_capacity AS has_capacity FROM resource_summary WHERE ${filter} ORDER BY ${order} \ No newline at end of file diff --git a/shongo-controller/src/test/java/cz/cesnet/shongo/controller/AbstractControllerTest.java b/shongo-controller/src/test/java/cz/cesnet/shongo/controller/AbstractControllerTest.java index 341a0a091..9d50ed7f8 100644 --- a/shongo-controller/src/test/java/cz/cesnet/shongo/controller/AbstractControllerTest.java +++ b/shongo-controller/src/test/java/cz/cesnet/shongo/controller/AbstractControllerTest.java @@ -14,7 +14,6 @@ import cz.cesnet.shongo.controller.notification.NotificationManager; import cz.cesnet.shongo.controller.scheduler.Preprocessor; import cz.cesnet.shongo.controller.scheduler.Scheduler; -import cz.cesnet.shongo.controller.util.DatabaseHelper; import cz.cesnet.shongo.controller.util.NativeQuery; import cz.cesnet.shongo.jade.Container; import org.joda.time.DateTime; @@ -24,8 +23,6 @@ import org.slf4j.LoggerFactory; import javax.persistence.EntityManager; -import javax.persistence.EntityManagerFactory; -import javax.persistence.Query; import java.util.*; /** @@ -340,7 +337,7 @@ public void stop() logger.debug("Starting controller for " + getClass().getName() + "..."); controller.start(); controller.startRpc(); - controller.startInterDomainRESTApi(); + controller.startRESTApi(); // Start client controllerClient = new ControllerClient(controller.getRpcHost(), controller.getRpcPort()); @@ -822,4 +819,11 @@ protected void checkSpecificationSummaryConsistency() entityManager.close(); } } + + protected String createTestResource() { + Resource resource = new Resource(); + resource.setName("resource"); + resource.setAllocatable(true); + return createResource(resource); + } } 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/DummyAuthorization.java b/shongo-controller/src/test/java/cz/cesnet/shongo/controller/DummyAuthorization.java index 87595f13f..8b561c00a 100644 --- a/shongo-controller/src/test/java/cz/cesnet/shongo/controller/DummyAuthorization.java +++ b/shongo-controller/src/test/java/cz/cesnet/shongo/controller/DummyAuthorization.java @@ -5,6 +5,7 @@ import cz.cesnet.shongo.controller.api.SecurityToken; import cz.cesnet.shongo.controller.authorization.AdministrationMode; import cz.cesnet.shongo.controller.authorization.Authorization; +import cz.cesnet.shongo.controller.authorization.ReservationDeviceConfig; import cz.cesnet.shongo.controller.authorization.UserAuthorizationData; import cz.cesnet.shongo.controller.authorization.UserData; import org.slf4j.Logger; @@ -61,6 +62,11 @@ public class DummyAuthorization extends Authorization */ private final Map> userIdsInGroup = new HashMap>(); + /** + * List of reservation devices, that would come from the configuration. + */ + private final Collection reservationDevices = new ArrayList<>(); + /** * Static initialization. */ @@ -320,6 +326,11 @@ public void onRemoveGroupUser(String groupId, String userId) userIds.remove(userId); } + @Override + public Collection listReservationDevices() { + return reservationDevices; + } + /** * @param configuration to be used for initialization * @param entityManagerFactory @@ -333,4 +344,15 @@ public static DummyAuthorization createInstance(ControllerConfiguration configur Authorization.setInstance(authorization); return authorization; } + + public void addReservationDevice(ReservationDeviceConfig reservationDeviceConfig) { + String deviceId = reservationDeviceConfig.getDeviceId(); + String accessToken = reservationDeviceConfig.getAccessToken(); + UserData userData = reservationDeviceConfig.getUserData(); + + userDataById.put(deviceId, userData); + userDataByAccessToken.put(accessToken, userData); + + reservationDevices.add(reservationDeviceConfig); + } } diff --git a/shongo-controller/src/test/java/cz/cesnet/shongo/controller/api/rpc/ReservationServiceImplTest.java b/shongo-controller/src/test/java/cz/cesnet/shongo/controller/api/rpc/ReservationServiceImplTest.java new file mode 100644 index 000000000..aae4eb2c5 --- /dev/null +++ b/shongo-controller/src/test/java/cz/cesnet/shongo/controller/api/rpc/ReservationServiceImplTest.java @@ -0,0 +1,95 @@ +package cz.cesnet.shongo.controller.api.rpc; + +import cz.cesnet.shongo.controller.AbstractControllerTest; +import cz.cesnet.shongo.controller.ReservationRequestPurpose; +import cz.cesnet.shongo.controller.ReservationRequestReusement; +import cz.cesnet.shongo.controller.api.ReservationRequest; +import cz.cesnet.shongo.controller.api.ReservationRequestSummary; +import cz.cesnet.shongo.controller.api.ResourceSpecification; +import cz.cesnet.shongo.controller.api.SecurityToken; +import cz.cesnet.shongo.controller.api.request.ListResponse; +import cz.cesnet.shongo.controller.api.request.ReservationRequestListRequest; +import cz.cesnet.shongo.controller.authorization.ReservationDeviceConfig; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class ReservationServiceImplTest extends AbstractControllerTest { + @Test + public void shouldListReservationsForResourceManagedByDevice() throws Exception { + ReservationService reservationService = getReservationService(); + + String resourceId = createTestResource(); + String reservationRequestId = allocateTestReservationRequest(resourceId); + + // Create reservation device that manages test resource. + ReservationDeviceConfig deviceConfig = new ReservationDeviceConfig("test", "test", resourceId); + getAuthorization().addReservationDevice(deviceConfig); + SecurityToken deviceToken = new SecurityToken(deviceConfig.getAccessToken()); + deviceToken.setUserInformation(deviceToken.getUserInformation()); + + // List resources with device's security token. + ReservationRequestListRequest listRequest = new ReservationRequestListRequest(deviceToken); + ListResponse response = reservationService.listReservationRequests(listRequest); + ReservationRequestSummary firstItem = response.getItem(0); + + // Assert that there is only the test reservation request in response. + assertEquals(response.getCount(), 1); + assertEquals(reservationRequestId, firstItem.getId()); + } + + @Test + public void shouldNotListReservationsForResourceNotManagedByDevice() throws Exception { + ReservationService reservationService = getReservationService(); + + String resourceId = createTestResource(); + allocateTestReservationRequest(resourceId); + + // Create security token for device that doesn't manage the test resource. + String anotherResourceId = createTestResource(); + ReservationDeviceConfig deviceConfig = new ReservationDeviceConfig("test", "test", anotherResourceId); + getAuthorization().addReservationDevice(deviceConfig); + SecurityToken deviceToken = new SecurityToken(deviceConfig.getAccessToken()); + deviceToken.setUserInformation(deviceToken.getUserInformation()); + + // List resources with device's security token. + ReservationRequestListRequest listRequest = new ReservationRequestListRequest(deviceToken); + ListResponse response = reservationService.listReservationRequests(listRequest); + + // Assert that there is only the test reservation request in response. + assertEquals(response.getCount(), 0); + } + + @Test + public void shouldCreateReservationRequestForManagedResource() { + ReservationService reservationService = getReservationService(); + + String resourceId = createTestResource(); + + // Create reservation device that manages test resource. + ReservationDeviceConfig deviceConfig = new ReservationDeviceConfig("test", "test", resourceId); + getAuthorization().addReservationDevice(deviceConfig); + SecurityToken deviceToken = new SecurityToken(deviceConfig.getAccessToken()); + deviceToken.setUserInformation(deviceToken.getUserInformation()); + + ReservationRequest reservationRequest = createTestReservationRequest(resourceId); + + String reservationRequestId = reservationService.createReservationRequest(deviceToken, reservationRequest); + + assertNotNull(reservationRequestId); + } + + private String allocateTestReservationRequest(String resourceId) throws Exception { + return allocate(createTestReservationRequest(resourceId)); + } + + private ReservationRequest createTestReservationRequest(String resourceId) { + ReservationRequest reservationRequest = new ReservationRequest(); + reservationRequest.setSlot("2012-01-01T00:00", "P1Y"); + reservationRequest.setSpecification(new ResourceSpecification(resourceId)); + reservationRequest.setReusement(ReservationRequestReusement.OWNED); + reservationRequest.setPurpose(ReservationRequestPurpose.USER); + return reservationRequest; + } +} diff --git a/shongo-controller/src/test/java/cz/cesnet/shongo/controller/authorization/AuthorizationTest.java b/shongo-controller/src/test/java/cz/cesnet/shongo/controller/authorization/AuthorizationTest.java index 686889cb0..15caa218e 100644 --- a/shongo-controller/src/test/java/cz/cesnet/shongo/controller/authorization/AuthorizationTest.java +++ b/shongo-controller/src/test/java/cz/cesnet/shongo/controller/authorization/AuthorizationTest.java @@ -2,6 +2,7 @@ import cz.cesnet.shongo.AliasType; import cz.cesnet.shongo.Technology; +import cz.cesnet.shongo.api.UserInformation; import cz.cesnet.shongo.controller.*; import cz.cesnet.shongo.controller.api.*; import cz.cesnet.shongo.controller.api.request.AclEntryListRequest; @@ -9,18 +10,20 @@ import cz.cesnet.shongo.controller.api.request.ObjectPermissionListRequest; import cz.cesnet.shongo.controller.api.request.ReservationListRequest; import cz.cesnet.shongo.controller.api.rpc.AuthorizationService; -import cz.cesnet.shongo.controller.booking.datetime.*; -import cz.cesnet.shongo.controller.booking.datetime.PeriodicDateTimeSlot; import org.joda.time.DateTime; import org.joda.time.Interval; import org.junit.Assert; import org.junit.Test; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + /** * Tests for creating, updating and deleting {@link cz.cesnet.shongo.controller.api.AclEntry}s. * @@ -81,12 +84,12 @@ public void testResource() throws Exception String resourceId = createResource(SECURITY_TOKEN, resource); aclEntries.add(new AclEntry(userId, resourceId, ObjectRole.OWNER)); - Assert.assertEquals(aclEntries, getAclEntries()); + assertEquals(aclEntries, getAclEntries()); getResourceService().deleteResource(SECURITY_TOKEN, resourceId); aclEntries.remove(new AclEntry(userId, resourceId, ObjectRole.OWNER)); - Assert.assertEquals(0, aclEntries.size()); + assertEquals(0, aclEntries.size()); } @Test @@ -103,7 +106,7 @@ public void testReservationRequest() throws Exception String resourceId = createResource(SECURITY_TOKEN, resource); aclEntries.add(new AclEntry(userId, resourceId, ObjectRole.OWNER)); - Assert.assertEquals(aclEntries, getAclEntries()); + assertEquals(aclEntries, getAclEntries()); ReservationRequest reservationRequest = new ReservationRequest(); reservationRequest.setSlot("2013-01-01T12:00", "PT2H"); @@ -114,13 +117,13 @@ public void testReservationRequest() throws Exception aclEntries.add(new AclEntry(userId, reservationRequestId, ObjectRole.OWNER)); aclEntries.add(new AclEntry(userId, reservation.getId(), ObjectRole.OWNER)); - Assert.assertEquals(aclEntries, getAclEntries()); + assertEquals(aclEntries, getAclEntries()); deleteAclEntry(userId, reservationRequestId, ObjectRole.OWNER); aclEntries.clear(); aclEntries.add(new AclEntry(userId, resourceId, ObjectRole.OWNER)); - Assert.assertEquals(aclEntries, getAclEntries()); + assertEquals(aclEntries, getAclEntries()); reservationRequest = new ReservationRequest(); reservationRequest.setSlot("2013-01-02T12:00", "PT2H"); @@ -144,13 +147,13 @@ public void testReservationRequest() throws Exception aclEntries.add(new AclEntry(userId, aliasReservation2.getId(), ObjectRole.OWNER)); aclEntries.add(new AclEntry(userId, valueReservation1, ObjectRole.OWNER)); aclEntries.add(new AclEntry(userId, valueReservation2, ObjectRole.OWNER)); - Assert.assertEquals(aclEntries, getAclEntries()); + assertEquals(aclEntries, getAclEntries()); deleteAclEntry(userId, reservationRequestId, ObjectRole.OWNER); aclEntries.clear(); aclEntries.add(new AclEntry(userId, resourceId, ObjectRole.OWNER)); - Assert.assertEquals(aclEntries, getAclEntries()); + assertEquals(aclEntries, getAclEntries()); } @Test @@ -222,8 +225,8 @@ public void testReusedReservationRequest() throws Exception String reservationRequest2Id = allocate(SECURITY_TOKEN_USER1, reservationRequest2); ExistingReservation reservation2 = (ExistingReservation) checkAllocated(reservationRequest2Id); - Assert.assertEquals(aliasReservation.getId(), reservation1.getReservation().getId()); - Assert.assertEquals(aliasReservation.getId(), reservation2.getReservation().getId()); + assertEquals(aliasReservation.getId(), reservation1.getReservation().getId()); + assertEquals(aliasReservation.getId(), reservation2.getReservation().getId()); getAuthorizationService().createAclEntry(SECURITY_TOKEN_USER1, new AclEntry(user2Id, reservationRequest1Id, ObjectRole.OWNER)); @@ -310,6 +313,58 @@ public void multipleAclRequestSpeedTest() throws Exception Assert.assertTrue("List all is not quicker than by one.", listPermissionsTime < listPermissionsByOneTime); } + @Test + public void shouldGetUserDataOfReservationDevice() { + DummyAuthorization authorization = getAuthorization(); + + String resourceId = createTestResource(); + ReservationDeviceConfig deviceConfig = new ReservationDeviceConfig("test", "test", resourceId); + authorization.addReservationDevice(deviceConfig); + + assertEquals(authorization.getUserData(deviceConfig.getDeviceId()), deviceConfig.getUserData()); + } + + @Test + public void shouldGetUserInformationOfReservationDeviceById() { + DummyAuthorization authorization = getAuthorization(); + + String resourceId = createTestResource(); + ReservationDeviceConfig deviceConfig = new ReservationDeviceConfig("test", "test", resourceId); + authorization.addReservationDevice(deviceConfig); + + assertEquals(authorization.getUserInformation(deviceConfig.getDeviceId()), deviceConfig.getUserData().getUserInformation()); + } + + @Test + public void shouldGetUserInformationOfReservationDeviceByToken() { + DummyAuthorization authorization = getAuthorization(); + + String resourceId = createTestResource(); + ReservationDeviceConfig deviceConfig = new ReservationDeviceConfig("test", "test", resourceId); + authorization.addReservationDevice(deviceConfig); + UserInformation userInformation = deviceConfig.getUserData().getUserInformation(); + + SecurityToken securityToken = new SecurityToken(deviceConfig.getAccessToken()); + securityToken.setUserInformation(userInformation); + + assertEquals(authorization.getUserInformation(securityToken), userInformation); + } + + @Test + public void shouldListReservationDeviceUserInformation() { + DummyAuthorization authorization = getAuthorization(); + + String resourceId = createTestResource(); + ReservationDeviceConfig deviceConfig = new ReservationDeviceConfig("test", "test", resourceId); + authorization.addReservationDevice(deviceConfig); + + Set reservationDevices = new HashSet<>(); + reservationDevices.add(deviceConfig.getDeviceId()); + + Collection userInformationResult = authorization.listUserInformation(reservationDevices, null); + assertTrue(userInformationResult.contains(deviceConfig.getUserData().getUserInformation())); + } + /** * @return collection of all {@link cz.cesnet.shongo.controller.api.AclEntry} for user with {@link #SECURITY_TOKEN} * @throws Exception 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..2a8b912b4 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 @@ -1,5 +1,7 @@ package cz.cesnet.shongo.controller.booking.request; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import cz.cesnet.shongo.AliasType; import cz.cesnet.shongo.Technology; import cz.cesnet.shongo.api.Alias; @@ -8,17 +10,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 +26,76 @@ */ public class ReservationRequestModificationTest extends AbstractControllerTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + public void testModifyAttributes() throws JsonProcessingException { + 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, objectMapper.readTree("[\"karnis@cenet.cz\", \"filip.karnis@cesnet.cz\"]")), + new AuxiliaryData("tag2", false, objectMapper.readTree("[\"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..dd26f7ac7 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 @@ -1,5 +1,8 @@ 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.api.util.DeviceAddress; import cz.cesnet.shongo.controller.*; import cz.cesnet.shongo.controller.api.*; @@ -21,6 +24,48 @@ * @author: Ondřej Pavelka */ public class TagTest extends AbstractControllerTest { + + @Test + public void testCreateTag() throws JsonProcessingException { + final ObjectMapper objectMapper = new ObjectMapper(); + + final String tagName1 = "testTag1"; + final String tagName2 = "testTag2"; + final TagType tagType2 = TagType.NOTIFY_EMAIL; + final JsonNode tagData2 = objectMapper.readTree("[\"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.assertNull(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-controller/src/test/java/cz/cesnet/shongo/controller/domains/InterDomainTest.java b/shongo-controller/src/test/java/cz/cesnet/shongo/controller/domains/InterDomainTest.java index c02d2608e..07fcd410f 100644 --- a/shongo-controller/src/test/java/cz/cesnet/shongo/controller/domains/InterDomainTest.java +++ b/shongo-controller/src/test/java/cz/cesnet/shongo/controller/domains/InterDomainTest.java @@ -27,7 +27,8 @@ public class InterDomainTest extends AbstractControllerTest private static final Integer INTERDOMAIN_LOCAL_PORT = 8443; private static final String INTERDOMAIN_LOCAL_PASSWORD = "shongo_test"; private static final String INTERDOMAIN_LOCAL_PASSWORD_HASH = SSLCommunication.hashPassword(INTERDOMAIN_LOCAL_PASSWORD.getBytes()); - private static final String TEST_CERT_PATH = "./shongo-controller/src/test/resources/keystore/server.crt"; + private static final String TEST_KEY_STORE_PATH = "keystore/server.p12"; + private static final String TEST_CERT_PATH = "keystore/server.crt"; private Domain loopbackDomain; private Long loopbackDomainId; @@ -35,11 +36,11 @@ public class InterDomainTest extends AbstractControllerTest @Override public void before() throws Exception { - System.setProperty(ControllerConfiguration.INTERDOMAIN_HOST, INTERDOMAIN_LOCAL_HOST); - System.setProperty(ControllerConfiguration.INTERDOMAIN_PORT, INTERDOMAIN_LOCAL_PORT.toString()); - System.setProperty(ControllerConfiguration.INTERDOMAIN_SSL_KEY_STORE, "./shongo-controller/src/test/resources/keystore/server.p12"); - System.setProperty(ControllerConfiguration.INTERDOMAIN_SSL_KEY_STORE_PASSWORD, "shongo"); - System.setProperty(ControllerConfiguration.INTERDOMAIN_SSL_KEY_STORE_TYPE, "PKCS12"); + System.setProperty(ControllerConfiguration.REST_API_HOST, INTERDOMAIN_LOCAL_HOST); + System.setProperty(ControllerConfiguration.REST_API_PORT, INTERDOMAIN_LOCAL_PORT.toString()); + System.setProperty(ControllerConfiguration.REST_API_SSL_KEY_STORE, getProjectResourcePath(TEST_KEY_STORE_PATH)); + System.setProperty(ControllerConfiguration.REST_API_SSL_KEY_STORE_PASSWORD, "shongo"); + System.setProperty(ControllerConfiguration.REST_API_SSL_KEY_STORE_TYPE, "PKCS12"); System.setProperty(ControllerConfiguration.INTERDOMAIN_PKI_CLIENT_AUTH, "false"); System.setProperty(ControllerConfiguration.INTERDOMAIN_COMMAND_TIMEOUT, "PT10S"); System.setProperty(ControllerConfiguration.INTERDOMAIN_BASIC_AUTH_PASSWORD, INTERDOMAIN_LOCAL_PASSWORD); @@ -51,7 +52,7 @@ public void before() throws Exception loopbackDomain.setName(LocalDomain.getLocalDomainName()); loopbackDomain.setOrganization("CESNET z.s.p.o."); loopbackDomain.setAllocatable(true); - loopbackDomain.setCertificatePath(TEST_CERT_PATH); + loopbackDomain.setCertificatePath(getProjectResourcePath(TEST_CERT_PATH)); DeviceAddress deviceAddress = new DeviceAddress(INTERDOMAIN_LOCAL_HOST, INTERDOMAIN_LOCAL_PORT); loopbackDomain.setDomainAddress(deviceAddress); loopbackDomain.setPasswordHash(INTERDOMAIN_LOCAL_PASSWORD_HASH); @@ -63,11 +64,11 @@ public void before() throws Exception @After public void tearDown() throws Exception { - System.clearProperty(ControllerConfiguration.INTERDOMAIN_HOST); - System.clearProperty(ControllerConfiguration.INTERDOMAIN_PORT); - System.clearProperty(ControllerConfiguration.INTERDOMAIN_SSL_KEY_STORE); - System.clearProperty(ControllerConfiguration.INTERDOMAIN_SSL_KEY_STORE_PASSWORD); - System.clearProperty(ControllerConfiguration.INTERDOMAIN_SSL_KEY_STORE_TYPE); + System.clearProperty(ControllerConfiguration.REST_API_HOST); + System.clearProperty(ControllerConfiguration.REST_API_PORT); + System.clearProperty(ControllerConfiguration.REST_API_SSL_KEY_STORE); + System.clearProperty(ControllerConfiguration.REST_API_SSL_KEY_STORE_PASSWORD); + System.clearProperty(ControllerConfiguration.REST_API_SSL_KEY_STORE_TYPE); System.clearProperty(ControllerConfiguration.INTERDOMAIN_PKI_CLIENT_AUTH); System.clearProperty(ControllerConfiguration.INTERDOMAIN_COMMAND_TIMEOUT); System.clearProperty(ControllerConfiguration.INTERDOMAIN_BASIC_AUTH_PASSWORD); @@ -449,4 +450,9 @@ protected DomainService getDomainService() { return InterDomainAgent.getInstance().getDomainService(); } + + private String getProjectResourcePath(String relativePath) + { + return Objects.requireNonNull(getClass().getClassLoader().getResource(relativePath)).getPath(); + } } diff --git a/shongo-controller/src/test/java/cz/cesnet/shongo/controller/rest/controllers/ReservationDeviceControllerTest.java b/shongo-controller/src/test/java/cz/cesnet/shongo/controller/rest/controllers/ReservationDeviceControllerTest.java new file mode 100644 index 000000000..c111b8c32 --- /dev/null +++ b/shongo-controller/src/test/java/cz/cesnet/shongo/controller/rest/controllers/ReservationDeviceControllerTest.java @@ -0,0 +1,42 @@ +package cz.cesnet.shongo.controller.rest.controllers; + +import cz.cesnet.shongo.controller.AbstractControllerTest; +import cz.cesnet.shongo.controller.api.ReservationDevice; +import cz.cesnet.shongo.controller.api.SecurityToken; +import cz.cesnet.shongo.controller.api.rpc.AuthorizationService; +import cz.cesnet.shongo.controller.authorization.ReservationDeviceConfig; +import cz.cesnet.shongo.controller.rest.models.reservationdevice.ReservationDeviceModel; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.http.ResponseEntity; + +import static org.junit.Assert.assertEquals; + +public class ReservationDeviceControllerTest extends AbstractControllerTest { + private final AuthorizationService authorizationService = Mockito.mock(AuthorizationService.class); + private final ReservationDeviceController controller = new ReservationDeviceController(authorizationService); + + @Test + public void shouldReturnDeviceInfo() { + String resourceId = createTestResource(); + ReservationDeviceConfig deviceConfig = new ReservationDeviceConfig("test", "test", resourceId); + getAuthorization().addReservationDevice(deviceConfig); + + SecurityToken deviceToken = new SecurityToken(deviceConfig.getAccessToken()); + ReservationDevice device = new ReservationDevice(); + device.setId(deviceConfig.getDeviceId()); + device.setAccessToken(deviceConfig.getAccessToken()); + device.setResourceId(deviceConfig.getResourceId()); + ReservationDeviceModel model = new ReservationDeviceModel(device); + + Mockito.when(authorizationService.getReservationDevice(deviceToken)).thenReturn(device); + + assertEquals(ResponseEntity.ok(model), controller.getReservationDevice(deviceToken)); + } + + @Test + public void shouldReturnNotFound() { + Mockito.when(authorizationService.getReservationDevice(Mockito.any())).thenReturn(null); + assertEquals(ResponseEntity.notFound().build(), controller.getReservationDevice(new SecurityToken("test"))); + } +} diff --git a/shongo-deployment/README.md b/shongo-deployment/README.md index 8848d0128..7838cd359 100644 --- a/shongo-deployment/README.md +++ b/shongo-deployment/README.md @@ -6,7 +6,7 @@ This directory can contain runtime files for Shongo components. To install Shongo use the following command: - service/shongo-install.sh shongo-controller shongo-connector shongo-client-web + service/shongo-install.sh shongo-controller shongo-connector To uninstall Shongo use the following command: @@ -17,7 +17,6 @@ To uninstall Shongo use the following command: Create the following configuration files to configure Shongo components: * shongo-client-cli.cfg.xml to configure Command-Line Client -* shongo-client-web.cfg.xml to configure Web Interface * shongo-client-connector.cfg.xml to configure connectors * shongo-client-connector.auth.xml to configure authentication of connectors * shongo-client-controller.auth.xml to configure Controller @@ -30,4 +29,4 @@ Use the following command to run Command-Line Client: Use the following command to check status of Shongo component (useful for Nagios NRPE plugin): - bin/shongo-check.sh [shongo-connector|shongo-controller|shongo-client-web] + bin/shongo-check.sh [shongo-connector|shongo-controller] diff --git a/shongo-deployment/bin/shongo-check.sh b/shongo-deployment/bin/shongo-check.sh index ca215a27b..c568c41cd 100755 --- a/shongo-deployment/bin/shongo-check.sh +++ b/shongo-deployment/bin/shongo-check.sh @@ -2,7 +2,7 @@ # # Check Shongo applications. Can be used in nagios NRPE plugin. # -# check_shongo.sh +# check_shongo.sh # BIN=$(dirname $0) @@ -65,12 +65,6 @@ function check_connector fi } -function check_client_web -{ - echo TODO: check client web - exit 3 -} - OPTIND=0 while getopts "c:" option; do case $option in @@ -85,9 +79,6 @@ case $1 in shongo-connector) check_connector $2 ;; - shongo-client-web) - check_client_web - ;; *) check_controller ;; diff --git a/shongo-deployment/bin/shongo-client-web-logins.sh b/shongo-deployment/bin/shongo-client-web-logins.sh deleted file mode 100755 index 6c466a96f..000000000 --- a/shongo-deployment/bin/shongo-client-web-logins.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash - -# Parse arguments -while test $# -gt 0 -do - case "$1" in - --unique) - UNIQUE=1 - ;; - --help) - cat < $TMP_FILE - -# Show requested results -if [[ $UNIQUE ]]; then - cat $TMP_FILE | sort | uniq -else - cat $TMP_FILE -fi -rm $TMP_FILE \ No newline at end of file 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; diff --git a/shongo-deployment/service/shongo-client-web.sh b/shongo-deployment/service/shongo-client-web.sh deleted file mode 100755 index 672f12b7c..000000000 --- a/shongo-deployment/service/shongo-client-web.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -NAME=shongo-client-web -BIN="java -Dfile.encoding=UTF-8 -jar ../shongo-client-web/target/shongo-client-web-:VERSION:.jar --daemon" -BIN_STARTED="ClientWeb successfully started" - -source $(dirname $0)/shongo-service.sh \ No newline at end of file diff --git a/shongo-deployment/service/shongo-install.sh b/shongo-deployment/service/shongo-install.sh index e017723eb..2408ac96c 100755 --- a/shongo-deployment/service/shongo-install.sh +++ b/shongo-deployment/service/shongo-install.sh @@ -14,7 +14,6 @@ fi # Parse arguments SHONGO_CONTROLLER=false SHONGO_CONNECTOR=false -SHONGO_CLIENT_WEB=false for argument in "$@" do case "$argument" in @@ -24,9 +23,6 @@ do shongo-connector) SHONGO_CONNECTOR=true ;; - shongo-client-web) - SHONGO_CLIENT_WEB=true - ;; esac done @@ -35,7 +31,6 @@ if [[ $# -eq 0 ]] ; then echo Installing all shongo services... SHONGO_CONTROLLER=true SHONGO_CONNECTOR=true - SHONGO_CLIENT_WEB=true fi ################################################################################ @@ -110,42 +105,6 @@ update-rc.d shongo-connector defaults 91 10 fi -################################################################################ -# -# Install shongo-client-web service -# -if [ "$SHONGO_CLIENT_WEB" = true ] ; then - -echo Installing shongo-client-web... -cat > $SERVICE_DIR/shongo-client-web < + +REST API settings + + + OPTIONAL rest-api/host + + Specifies on which host should the REST API listen. + If not set, the default host localhost will be used. + + + + OPTIONAL rest-api/port + + Specifies port on which the REST API will listen. + If not set, the default port 9999 will be used. + + + + OPTIONAL rest-api/origin + + Specifies one allowed HTTP request origins. + Used for Cross-Origin Resource Sharing (CORS). + + + + OPTIONAL rest-api/ssl-key-store + + Specifies filename of Java KeyStore (JKS) with SSL certificate to use for secured REST API (HTTP) communication. + If not set the SSL is not used for REST API communication. + + + + OPTIONAL rest-api/ssl-key-store-password + + Specifies password for JKS. + + + + + Example: + + 127.0.0.1 + 8080 + https://meetings.cesnet.cz + shongo-dev.cesnet.cz:4200 + +]]> + + + SSL settings SSL settings can be used to turn off or alter hostname verification for specified hostnames. @@ -572,6 +623,13 @@ service postgresql restart (password) + + meetings.cesnet.cz + 8080 + https://meetings.cesnet.cz + shongo-dev.cesnet.cz:4200 + + no-reply@meetings.cesnet.cz rs.cesnet.cz