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