+ #if($att_item.contentType.startsWith($MIME_TYPE_MICROSOFT))
+
+ #end
$!att_item.hoverText
$formattedText.escapeHtml($att_item.displayName)
@@ -599,6 +602,8 @@ $(document).ready(function () {
$(this).addClass('googlethumbnailsmall');
$(this).removeClass('googlethumbnailbig');
});
+
+ $('.nav-item.active').find('a').focus();
});
if ( $(window).width() > portalSmallBreakPoint() ) { ## Don't go modal when mobile..
diff --git a/content/content-types/src/java/org/sakaiproject/content/types/FileUploadType.java b/content/content-types/src/java/org/sakaiproject/content/types/FileUploadType.java
index d986dd4af4e7..72537c92c4da 100644
--- a/content/content-types/src/java/org/sakaiproject/content/types/FileUploadType.java
+++ b/content/content-types/src/java/org/sakaiproject/content/types/FileUploadType.java
@@ -111,6 +111,9 @@ public String getIconLocation(ContentEntity entity) {
if (entity instanceof ContentResource) {
String mimetype = ((ContentResource) entity).getContentType();
if (mimetype != null && !"".equals(mimetype.trim())) {
+ if(mimetype.startsWith(ResourceType.MIME_TYPE_MICROSOFT)) {
+ mimetype = mimetype.replaceFirst(ResourceType.MIME_TYPE_MICROSOFT, "");
+ }
iconLocation = contentTypeImageService.getContentTypeImage(mimetype);
}
}
@@ -123,6 +126,9 @@ public String getIconClass(ContentEntity entity) {
if (entity instanceof ContentResource) {
String mimetype = ((ContentResource) entity).getContentType();
if (mimetype != null && !"".equals(mimetype.trim())) {
+ if(mimetype.startsWith(ResourceType.MIME_TYPE_MICROSOFT)) {
+ mimetype = mimetype.replaceFirst(ResourceType.MIME_TYPE_MICROSOFT, "");
+ }
iconClass = contentTypeImageService.getContentTypeImageClass(mimetype);
}
}
@@ -145,6 +151,9 @@ public String getLocalizedHoverText(ContentEntity entity) {
if (entity instanceof ContentResource) {
String mimetype = ((ContentResource) entity).getContentType();
if (mimetype != null && !"".equals(mimetype.trim())) {
+ if(mimetype.startsWith(ResourceType.MIME_TYPE_MICROSOFT)) {
+ mimetype = mimetype.replaceFirst(ResourceType.MIME_TYPE_MICROSOFT, "");
+ }
hoverText = contentTypeImageService.getContentTypeDisplayName(mimetype);
}
}
diff --git a/kernel/api/src/main/java/org/sakaiproject/content/api/ResourceType.java b/kernel/api/src/main/java/org/sakaiproject/content/api/ResourceType.java
index b85f33d0a832..916570802963 100644
--- a/kernel/api/src/main/java/org/sakaiproject/content/api/ResourceType.java
+++ b/kernel/api/src/main/java/org/sakaiproject/content/api/ResourceType.java
@@ -40,6 +40,7 @@ public interface ResourceType
public static final String MIME_TYPE_HTML = "text/html";
public static final String MIME_TYPE_METAOBJ = "application/x-osp";
public static final String MIME_TYPE_URL = "text/url";
+ public static final String MIME_TYPE_MICROSOFT = "microsoft/";
public static final int EXPANDABLE_FOLDER_SIZE_LIMIT = 256;
diff --git a/kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessage.java b/kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessage.java
new file mode 100644
index 000000000000..8c9f913bfa8b
--- /dev/null
+++ b/kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessage.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2023 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.messaging.api;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import lombok.Builder;
+import lombok.Data;
+
+@Builder
+@Data
+public class MicrosoftMessage {
+ public enum Topic {
+ CREATE_ELEMENT,
+ DELETE_ELEMENT,
+ MODIFY_ELEMENT,
+ ADD_MEMBER_TO_AUTHZGROUP,
+ REMOVE_MEMBER_FROM_AUTHZGROUP,
+ TEAM_CREATION,
+ CHANGE_LISTEN_GROUP_EVENTS
+ }
+
+ public enum Action {
+ CREATE, DELETE, ADD, REMOVE, REMOVE_ALL, ENABLE, DISABLE, UNPUBLISH;
+ }
+ public enum Type {
+ SITE, GROUP, TEAM;
+ }
+
+ private Action action;
+ private Type type;
+ private String reference;
+ private String siteId;
+ private String groupId;
+ private String userId;
+ private boolean owner;
+ private int status;
+ @Builder.Default
+ private boolean force = false;
+
+ //custom builder
+ public static class MicrosoftMessageBuilder {
+ private static Pattern sitePattern = Pattern.compile("^/site/([^/]+)$");
+ private static Pattern groupPattern = Pattern.compile("^/site/([^/]+)/group/([^/]+)$");
+
+ //fill type, siteId and groupId based on reference
+ public MicrosoftMessageBuilder reference(String reference) {
+ this.reference = reference;
+
+ Matcher matcher = sitePattern.matcher(reference);
+ if(matcher.find()) {
+ this.type = Type.SITE;
+ this.siteId = matcher.group(1);
+ } else {
+ matcher = groupPattern.matcher(reference);
+ if(matcher.find()) {
+ this.type = Type.GROUP;
+ this.siteId = matcher.group(1);
+ this.groupId = matcher.group(2);
+ }
+ }
+ return this;
+ }
+ }
+}
diff --git a/cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveFolder.java b/kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessageListener.java
similarity index 62%
rename from cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveFolder.java
rename to kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessageListener.java
index 2fe23d51e493..5bfd20b0e57c 100644
--- a/cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveFolder.java
+++ b/kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessageListener.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2003-2019 The Apereo Foundation
+ * Copyright (c) 2023 The Apereo Foundation
*
* Licensed under the Educational Community License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,17 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.sakaiproject.onedrive.model;
+package org.sakaiproject.messaging.api;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+public interface MicrosoftMessageListener {
-import lombok.Getter;
-import lombok.Setter;
-import lombok.ToString;
-
-@Getter @Setter
-@ToString
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class OneDriveFolder {
- public Integer childCount;
+ public void read(MicrosoftMessage msg);
}
+
diff --git a/kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessagingService.java b/kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessagingService.java
new file mode 100644
index 000000000000..5d4b7bde2cb1
--- /dev/null
+++ b/kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessagingService.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2023 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.messaging.api;
+
+public interface MicrosoftMessagingService {
+
+ public void listen(MicrosoftMessage.Topic topic, MicrosoftMessageListener listener);
+ public void send(MicrosoftMessage.Topic topic, MicrosoftMessage msg);
+
+}
diff --git a/kernel/component-manager/src/main/bundle/org/sakaiproject/config/kernel.properties b/kernel/component-manager/src/main/bundle/org/sakaiproject/config/kernel.properties
index 9dcfed6c98f6..472d73966a24 100644
--- a/kernel/component-manager/src/main/bundle/org/sakaiproject/config/kernel.properties
+++ b/kernel/component-manager/src/main/bundle/org/sakaiproject/config/kernel.properties
@@ -282,8 +282,7 @@ realm.allowed..auth=annc.all.groups,annc.read,asn.read,asn.receive.notifications
rwiki.update,signup.attend,signup.attend.all,signup.view,signup.view.all,site.viewRoster,site.visit,sitestats.view,\
usermembership.view,lessonbuilder.read
-stealthTools@org.sakaiproject.tool.api.ActiveToolManager=
-
+stealthTools@org.sakaiproject.tool.api.ActiveToolManager=sakai.meetings,microsoft.mediagallery,microsoft.collaborativedocuments
# These tools need to use server saving method as they are on older JSF versions. If upgraded to JSf 1.2+ please remove from this list
# Podcasts Tool
jsf.state_saving_method.sakai-podcasts=server
diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/AuthzGroupServiceTest.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/AuthzGroupServiceTest.java
index 63a229b03cb8..ebb5a5d3e4c3 100644
--- a/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/AuthzGroupServiceTest.java
+++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/AuthzGroupServiceTest.java
@@ -27,6 +27,7 @@
import org.sakaiproject.db.api.SqlService;
import org.sakaiproject.entity.api.EntityManager;
import org.sakaiproject.event.api.EventTrackingService;
+import org.sakaiproject.messaging.api.MicrosoftMessagingService;
import org.sakaiproject.time.api.TimeService;
import org.sakaiproject.user.api.UserDirectoryService;
import org.sakaiproject.tool.api.SessionManager;
@@ -121,4 +122,9 @@ public Set getMaintainRoles() {
public String getGroupReference(String siteId, String groupId) {
return null;
}
+
+ @Override
+ protected MicrosoftMessagingService microsoftMessagingService() {
+ return null;
+ }
}
diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/BaseAuthzGroup.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/BaseAuthzGroup.java
index 864be90ba9ea..a6c913385f77 100644
--- a/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/BaseAuthzGroup.java
+++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/BaseAuthzGroup.java
@@ -44,6 +44,8 @@
import org.sakaiproject.entity.api.Reference;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.entity.api.ResourcePropertiesEdit;
+import org.sakaiproject.messaging.api.MicrosoftMessage;
+import org.sakaiproject.site.api.SiteService;
import org.sakaiproject.time.api.TimeService;
import org.sakaiproject.user.api.User;
import org.sakaiproject.user.api.UserDirectoryService;
@@ -1048,6 +1050,16 @@ public void addMember(String user, String roleId, boolean active, boolean provid
grant.active = active;
grant.provided = provided;
}
+
+
+ //send message to (ignite) MicrosoftMessagingService
+ this.baseAuthzGroupService.microsoftMessagingService().send(MicrosoftMessage.Topic.ADD_MEMBER_TO_AUTHZGROUP, MicrosoftMessage.builder()
+ .action(MicrosoftMessage.Action.ADD)
+ .reference(this.getId())
+ .userId(user)
+ .owner(role.isAllowed(SiteService.SECURE_UPDATE_SITE))
+ .build()
+ );
}
/**
@@ -1058,6 +1070,14 @@ public void removeMember(String user)
if (m_lazy) baseAuthzGroupService.m_storage.completeGet(this);
m_userGrants.remove(user);
+
+ //send message to (ignite) MicrosoftMessagingService
+ this.baseAuthzGroupService.microsoftMessagingService().send(MicrosoftMessage.Topic.REMOVE_MEMBER_FROM_AUTHZGROUP, MicrosoftMessage.builder()
+ .action(MicrosoftMessage.Action.REMOVE)
+ .reference(this.getId())
+ .userId(user)
+ .build()
+ );
}
/**
@@ -1079,6 +1099,13 @@ public void removeMembers()
if (m_lazy) baseAuthzGroupService.m_storage.completeGet(this);
m_userGrants.clear();
+
+ //send message to (ignite) MicrosoftMessagingService
+ this.baseAuthzGroupService.microsoftMessagingService().send(MicrosoftMessage.Topic.REMOVE_MEMBER_FROM_AUTHZGROUP, MicrosoftMessage.builder()
+ .action(MicrosoftMessage.Action.REMOVE_ALL)
+ .reference(this.getId())
+ .build()
+ );
}
/**
diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/BaseAuthzGroupService.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/BaseAuthzGroupService.java
index e5fcb5f81d07..eea4832895e6 100644
--- a/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/BaseAuthzGroupService.java
+++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/BaseAuthzGroupService.java
@@ -61,6 +61,7 @@
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.event.api.EventTrackingService;
import org.sakaiproject.javax.PagingPosition;
+import org.sakaiproject.messaging.api.MicrosoftMessagingService;
import org.sakaiproject.site.api.SiteService;
import org.sakaiproject.time.api.TimeService;
import org.sakaiproject.tool.api.SessionManager;
@@ -281,6 +282,8 @@ public void setRoleProvider(RoleProvider provider)
protected List authzGroupAdvisors;
+ protected abstract MicrosoftMessagingService microsoftMessagingService();
+
protected SiteService siteService;
public void setSiteService(SiteService siteService) {
this.siteService = siteService;
diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/BaseContentService.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/BaseContentService.java
index 00687c0949ee..085246a255ad 100644
--- a/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/BaseContentService.java
+++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/BaseContentService.java
@@ -5947,13 +5947,13 @@ public void commitResource(ContentResourceEdit edit, int priority) throws OverQu
boolean hasContentTypeAlready = hasContentType(edit.getId());
//use magic to fix mimetype
- //Don't process for special TYPE_URL type
+ //Don't process for special TYPE_URL or MICROSOFT type
String currentContentType = edit.getContentType();
m_useMimeMagic = m_serverConfigurationService.getBoolean("content.useMimeMagic", m_useMimeMagic);
m_ignoreExtensions = Arrays.asList(ArrayUtils.nullToEmpty(m_serverConfigurationService.getStrings("content.mimeMagic.ignorecontent.extensions")));
m_ignoreMimeTypes = Arrays.asList(ArrayUtils.nullToEmpty(m_serverConfigurationService.getStrings("content.mimeMagic.ignorecontent.mimetypes")));
- if (m_useMimeMagic && DETECTOR != null && !ResourceProperties.TYPE_URL.equals(currentContentType) && !hasContentTypeAlready) {
+ if (m_useMimeMagic && DETECTOR != null && !ResourceProperties.TYPE_URL.equals(currentContentType) && !currentContentType.startsWith(ResourceType.MIME_TYPE_MICROSOFT) && !hasContentTypeAlready) {
try (
TikaInputStream buff = TikaInputStream.get(edit.streamContent());
) {
@@ -6926,8 +6926,8 @@ protected void handleAccessResource(HttpServletRequest req, HttpServletResponse
res.addHeader("Last-Modified", rfc1123Date.format(lastModTime));
}
- // for url content type, encode a redirect to the body URL
- if (contentType.equalsIgnoreCase(ResourceProperties.TYPE_URL))
+ // for url or Microsoft content type, encode a redirect to the body URL
+ if (contentType.equalsIgnoreCase(ResourceProperties.TYPE_URL) || contentType.startsWith(ResourceType.MIME_TYPE_MICROSOFT))
{
if (len < MAX_URL_LENGTH) {
diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/messaging/impl/MicrosoftMessagingServiceImpl.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/messaging/impl/MicrosoftMessagingServiceImpl.java
new file mode 100644
index 000000000000..fc555255f72c
--- /dev/null
+++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/messaging/impl/MicrosoftMessagingServiceImpl.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2023 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.messaging.impl;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import javax.annotation.Resource;
+
+import org.apache.ignite.IgniteMessaging;
+import org.sakaiproject.ignite.EagerIgniteSpringBean;
+import org.sakaiproject.messaging.api.MicrosoftMessage;
+import org.sakaiproject.messaging.api.MicrosoftMessageListener;
+import org.sakaiproject.messaging.api.MicrosoftMessagingService;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class MicrosoftMessagingServiceImpl implements MicrosoftMessagingService {
+
+ @Resource
+ private EagerIgniteSpringBean ignite;
+
+ private IgniteMessaging messaging;
+
+ private ExecutorService executor;
+
+ public void init() {
+ log.info("Initializing Microsoft Messaging Service");
+
+ executor = Executors.newFixedThreadPool(20);
+ messaging = ignite.message(ignite.cluster().forLocal());
+ }
+
+ public void destroy() {
+ executor.shutdownNow();
+ }
+
+ public void listen(MicrosoftMessage.Topic topic, MicrosoftMessageListener listener) {
+ messaging.localListen(topic, (nodeId, message) -> {
+ executor.execute(() -> {
+ listener.read((MicrosoftMessage)message);
+ });
+ return true;
+ });
+ }
+
+ public void send(MicrosoftMessage.Topic topic, MicrosoftMessage msg) {
+ try {
+ messaging.send(topic, msg);
+ } catch(Exception e) {
+ log.error("Error sending MicrosoftMessage");
+ }
+ }
+
+}
diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/site/impl/BaseSite.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/site/impl/BaseSite.java
index 336b8bb0bf4a..af036580c612 100644
--- a/kernel/kernel-impl/src/main/java/org/sakaiproject/site/impl/BaseSite.java
+++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/site/impl/BaseSite.java
@@ -47,6 +47,8 @@
import org.sakaiproject.entity.api.Reference;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.entity.api.ResourcePropertiesEdit;
+import org.sakaiproject.messaging.api.MicrosoftMessage;
+import org.sakaiproject.messaging.api.MicrosoftMessagingService;
import org.sakaiproject.site.api.Group;
import org.sakaiproject.site.api.Site;
import org.sakaiproject.site.api.SitePage;
@@ -169,6 +171,7 @@ public class BaseSite implements Site
protected AuthzGroup m_azg = null;
private AuthzGroupService authzGroupService;
+ private MicrosoftMessagingService microsoftMessagingService;
private FormattedText formattedText;
/**
* Set to true if we have changed our azg, so it need to be written back on
@@ -577,6 +580,7 @@ private void setupServices(BaseSiteService siteService, SessionManager sessionMa
}
}
this.authzGroupService = this.siteService.authzGroupService();
+ this.microsoftMessagingService = this.siteService.microsoftMessagingService();
this.sessionManager = sessionManager;
if (this.sessionManager == null) {
this.sessionManager = (SessionManager) ComponentManager.get(SessionManager.class);
@@ -1550,7 +1554,16 @@ public void setJoinerRole(String role)
public void setPublished(boolean published)
{
m_published = published;
-
+
+ //send message to (ignite) MicrosoftMessagingService
+ if(!published) {
+ microsoftMessagingService.send(MicrosoftMessage.Topic.MODIFY_ELEMENT, MicrosoftMessage.builder()
+ .action(MicrosoftMessage.Action.UNPUBLISH)
+ .type(MicrosoftMessage.Type.SITE)
+ .siteId(this.getId())
+ .build()
+ );
+ }
}
/**
@@ -1717,6 +1730,15 @@ public Group addGroup()
{
Group rv = new BaseGroup(siteService, this);
m_groups.add(rv);
+
+ //send message to (ignite) MicrosoftMessagingService
+ microsoftMessagingService.send(MicrosoftMessage.Topic.CREATE_ELEMENT, MicrosoftMessage.builder()
+ .action(MicrosoftMessage.Action.CREATE)
+ .type(MicrosoftMessage.Type.GROUP)
+ .siteId(this.getId())
+ .groupId(rv.getId())
+ .build()
+ );
return rv;
}
@@ -1747,6 +1769,15 @@ public void deleteGroup(Group group) throws AuthzRealmLockException
// track so we can clean up related on commit
m_deletedGroups.add(group);
+
+ //send message to (ignite) MicrosoftMessagingService
+ microsoftMessagingService.send(MicrosoftMessage.Topic.DELETE_ELEMENT, MicrosoftMessage.builder()
+ .action(MicrosoftMessage.Action.DELETE)
+ .type(MicrosoftMessage.Type.GROUP)
+ .siteId(this.getId())
+ .groupId(group.getId())
+ .build()
+ );
}
/**
diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/site/impl/BaseSiteService.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/site/impl/BaseSiteService.java
index d8a96035ec32..b5a12f52ccde 100644
--- a/kernel/kernel-impl/src/main/java/org/sakaiproject/site/impl/BaseSiteService.java
+++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/site/impl/BaseSiteService.java
@@ -85,6 +85,8 @@
import org.sakaiproject.javax.PagingPosition;
import org.sakaiproject.memory.api.Cache;
import org.sakaiproject.memory.api.MemoryService;
+import org.sakaiproject.messaging.api.MicrosoftMessage;
+import org.sakaiproject.messaging.api.MicrosoftMessagingService;
import org.sakaiproject.site.api.AllowedJoinableAccount;
import org.sakaiproject.site.api.Group;
import org.sakaiproject.site.api.Site;
@@ -478,6 +480,12 @@ public void setCacheCleanerMinutes(String time)
* @return the NotificationService collaborator
*/
protected abstract NotificationService notificationService();
+
+ /**
+ *
+ * @return the MicrosoftMessagingService collaborator
+ */
+ protected abstract MicrosoftMessagingService microsoftMessagingService();
/**********************************************************************************************************************************************************************************************************************************************************
* Init and Destroy
@@ -1345,6 +1353,14 @@ public Site addSite(String id, String type) throws IdInvalidException, IdUsedExc
((BaseSite) site).setEvent(SECURE_ADD_SITE);
doSave((BaseSite) site, true);
+
+ //send message to (ignite) MicrosoftMessagingService
+ microsoftMessagingService().send(MicrosoftMessage.Topic.CREATE_ELEMENT, MicrosoftMessage.builder()
+ .action(MicrosoftMessage.Action.CREATE)
+ .type(MicrosoftMessage.Type.SITE)
+ .siteId(id)
+ .build()
+ );
return site;
}
@@ -1432,6 +1448,14 @@ public Site addSite(String id, Site other, String realmTemplate) throws IdInvali
((BaseSite) site).setEvent(SECURE_ADD_SITE);
doSave((BaseSite) site, true);
+
+ //send message to (ignite) MicrosoftMessagingService
+ microsoftMessagingService().send(MicrosoftMessage.Topic.CREATE_ELEMENT, MicrosoftMessage.builder()
+ .action(MicrosoftMessage.Action.CREATE)
+ .type(MicrosoftMessage.Type.SITE)
+ .siteId(id)
+ .build()
+ );
return site;
}
@@ -1527,6 +1551,14 @@ public void removeSite(Site site, boolean isHardDelete) throws PermissionExcepti
// Use the HardDelete interface to purge content from database
if (isHardDelete) {
hardDelete(site);
+
+ //send message to (ignite) MicrosoftMessagingService
+ microsoftMessagingService().send(MicrosoftMessage.Topic.DELETE_ELEMENT, MicrosoftMessage.builder()
+ .action(MicrosoftMessage.Action.DELETE)
+ .type(MicrosoftMessage.Type.SITE)
+ .siteId(site.getId())
+ .build()
+ );
}
}
diff --git a/kernel/kernel-impl/src/main/sql/mysql/sakai_site.sql b/kernel/kernel-impl/src/main/sql/mysql/sakai_site.sql
index 5d7b440d1d66..c8aaf57cef6e 100644
--- a/kernel/kernel-impl/src/main/sql/mysql/sakai_site.sql
+++ b/kernel/kernel-impl/src/main/sql/mysql/sakai_site.sql
@@ -235,6 +235,8 @@ INSERT INTO SAKAI_SITE_PAGE VALUES('~admin-1120', '~admin', 'Preferences', '0',
INSERT INTO SAKAI_SITE_TOOL VALUES('~admin-1125', '~admin-1120', '~admin', 'sakai.preferences', 1, 'Preferences', NULL );
INSERT INTO SAKAI_SITE_PAGE VALUES('~admin-1200', '~admin', 'User Membership', '0', 14, '0' );
INSERT INTO SAKAI_SITE_TOOL VALUES('~admin-1210', '~admin-1200', '~admin', 'sakai.usermembership', 1, 'User Membership', NULL );
+INSERT INTO SAKAI_SITE_PAGE VALUES('~admin-2000', '~admin', 'Microsoft Admin Tool', '0', 15, '0' );
+INSERT INTO SAKAI_SITE_TOOL VALUES('~admin-2000', '~admin-2000', '~admin', 'microsoft.admin', 1, 'Microsoft Admin Tool', NULL );
INSERT INTO SAKAI_SITE VALUES('!admin', 'Administration Workspace', null, null, 'Administration Workspace', null, null, null, 1, 0, 0, '', 'admin', 'admin', NOW(), NOW(), 0, 0, 0, 0, null);
INSERT INTO SAKAI_SITE_PAGE VALUES('!admin-100', '!admin', 'Home', '0', 1, '0' );
@@ -299,6 +301,9 @@ INSERT INTO SAKAI_SITE_PAGE_PROPERTY VALUES('!admin', '!admin-1575', 'sitePage.c
INSERT INTO SAKAI_SITE_PAGE VALUES('!admin-1590', '!admin', 'Academic Term Manager', '0', 22, '0' );
INSERT INTO SAKAI_SITE_TOOL VALUES('!admin-1590', '!admin-1590', '!admin', 'sakai.acadtermmanage', 1, 'Academic Term Manager', NULL );
INSERT INTO SAKAI_SITE_PAGE_PROPERTY VALUES('!admin', '!admin-1590', 'sitePage.customTitle', 'true');
+INSERT INTO SAKAI_SITE_PAGE VALUES('!admin-2000', '!admin', 'Microsoft Admin Tool', '0', 23, '0' );
+INSERT INTO SAKAI_SITE_TOOL VALUES('!admin-2000', '!admin-2000', '!admin', 'microsoft.admin', 1, 'Microsoft Admin Tool', NULL );
+INSERT INTO SAKAI_SITE_PAGE_PROPERTY VALUES('!admin', '!admin-2000', 'sitePage.customTitle', 'true');
INSERT INTO SAKAI_SITE_USER VALUES('!admin', 'admin', -1);
diff --git a/kernel/kernel-impl/src/main/webapp/WEB-INF/authz-components.xml b/kernel/kernel-impl/src/main/webapp/WEB-INF/authz-components.xml
index 5b533a7c4b4a..3b17dca591d5 100644
--- a/kernel/kernel-impl/src/main/webapp/WEB-INF/authz-components.xml
+++ b/kernel/kernel-impl/src/main/webapp/WEB-INF/authz-components.xml
@@ -23,6 +23,7 @@
+
${auto.ddl}
diff --git a/kernel/kernel-impl/src/main/webapp/WEB-INF/messaging-components.xml b/kernel/kernel-impl/src/main/webapp/WEB-INF/messaging-components.xml
index eed305555861..6bcdf673cedb 100644
--- a/kernel/kernel-impl/src/main/webapp/WEB-INF/messaging-components.xml
+++ b/kernel/kernel-impl/src/main/webapp/WEB-INF/messaging-components.xml
@@ -35,4 +35,9 @@
+
+
+
diff --git a/kernel/kernel-impl/src/main/webapp/WEB-INF/site-components.xml b/kernel/kernel-impl/src/main/webapp/WEB-INF/site-components.xml
index a36f922eae31..2d85f2d57461 100644
--- a/kernel/kernel-impl/src/main/webapp/WEB-INF/site-components.xml
+++ b/kernel/kernel-impl/src/main/webapp/WEB-INF/site-components.xml
@@ -29,6 +29,7 @@
+
${auto.ddl}
diff --git a/kernel/kernel-impl/src/test/java/org/sakaiproject/authz/impl/test/ContreteAuthzGroupService.java b/kernel/kernel-impl/src/test/java/org/sakaiproject/authz/impl/test/ContreteAuthzGroupService.java
index 2d8fc9fdf1bb..26d4310e1849 100644
--- a/kernel/kernel-impl/src/test/java/org/sakaiproject/authz/impl/test/ContreteAuthzGroupService.java
+++ b/kernel/kernel-impl/src/test/java/org/sakaiproject/authz/impl/test/ContreteAuthzGroupService.java
@@ -21,6 +21,7 @@
import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.entity.api.EntityManager;
import org.sakaiproject.event.api.EventTrackingService;
+import org.sakaiproject.messaging.api.MicrosoftMessagingService;
import org.sakaiproject.time.api.TimeService;
import org.sakaiproject.tool.api.SessionManager;
import org.sakaiproject.user.api.UserDirectoryService;
@@ -82,4 +83,10 @@ protected Storage newStorage() {
return null;
}
+ @Override
+ protected MicrosoftMessagingService microsoftMessagingService() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
}
diff --git a/kernel/kernel-impl/src/test/java/org/sakaiproject/site/impl/SiteServiceTest.java b/kernel/kernel-impl/src/test/java/org/sakaiproject/site/impl/SiteServiceTest.java
index 349baad997a3..7c5cf43c0ed1 100644
--- a/kernel/kernel-impl/src/test/java/org/sakaiproject/site/impl/SiteServiceTest.java
+++ b/kernel/kernel-impl/src/test/java/org/sakaiproject/site/impl/SiteServiceTest.java
@@ -36,6 +36,7 @@
import org.sakaiproject.id.api.IdManager;
import org.sakaiproject.javax.PagingPosition;
import org.sakaiproject.memory.api.MemoryService;
+import org.sakaiproject.messaging.api.MicrosoftMessagingService;
import org.sakaiproject.site.api.Site;
import org.sakaiproject.thread_local.api.ThreadLocalManager;
import org.sakaiproject.time.api.TimeService;
@@ -188,4 +189,10 @@ protected NotificationService notificationService() {
// TODO Auto-generated method stub
return null;
}
+
+ @Override
+ protected MicrosoftMessagingService microsoftMessagingService() {
+ // TODO Auto-generated method stub
+ return null;
+ }
}
diff --git a/library/src/skins/default/src/sass/base/_icons.scss b/library/src/skins/default/src/sass/base/_icons.scss
index 3a706d0ee2ed..3177d9b50ed5 100644
--- a/library/src/skins/default/src/sass/base/_icons.scss
+++ b/library/src/skins/default/src/sass/base/_icons.scss
@@ -102,6 +102,9 @@ $fa-font-path: "./fonts";
sakai-users : bi-people,
sakai-web-168 : bi-globe,
sakai-plus: bi-plus-square,
+ microsoft-admin : bi-microsoft,
+ microsoft-mediagallery : bi-collection-play,
+ microsoft-collaborativedocuments : bi-files,
// other tools
alerts : bi-bell,
kaltura-admin : bi-gear,
diff --git a/library/src/skins/default/src/sass/modules/tool/microsoft-admin/_microsoft-admin.scss b/library/src/skins/default/src/sass/modules/tool/microsoft-admin/_microsoft-admin.scss
new file mode 100644
index 000000000000..667e7879e1b2
--- /dev/null
+++ b/library/src/skins/default/src/sass/modules/tool/microsoft-admin/_microsoft-admin.scss
@@ -0,0 +1,250 @@
+.#{$namespace}microsoft-admin {
+
+ // General styles ------------------------
+ $background-color_1: var(--sakai-color-gray--lighter-7);
+
+ .microsoft-search{
+ font-family:Arial, FontAwesome;
+ margin-bottom: 15px;
+ }
+
+ div.page-header {
+ .header-title {
+ margin: revert;
+ }
+ }
+
+ .text-bold {
+ font-weight: 700;
+ }
+
+ div.table-row {
+ display: flex;
+ align-items: center;
+ border: 1px solid #ddd;
+ }
+
+ .table-space {
+ >div {
+ padding: 10px;
+ }
+ }
+
+ div.index-background {
+ background-color: $background-color_1;
+ }
+
+ a.my-info-class {
+ text-decoration: none;
+ }
+
+ input[type="checkbox"] {
+ margin: 0 !important;
+ }
+
+ .popover{
+ width: 250px;
+ }
+
+ .flex-column{
+ display:flex;
+ flex-direction: column;
+ }
+
+ .property-filter{
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 5px;
+ }
+
+
+ // Index styles -----------------
+ .rotate90 {
+ transform: rotate(90deg);
+ transition: transform 0.3s ease-in-out;
+ }
+
+ .no-h-padding {
+ padding-right: 0;
+ padding-left: 0;
+ }
+
+ .utility-gap {
+ gap: 15px;
+ }
+
+ .utility-container {
+ div {
+ justify-content: center;
+ }
+ }
+
+ div.search {
+ justify-content: flex-start;
+ }
+
+ .site-title-margin {
+ margin-left: 8px;
+ }
+
+ .input-search {
+ margin: 0 !important;
+ }
+
+
+ // Synch Row Styles ---------------------
+ .container-flex {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .group-table {
+ padding: 1em 2.2em;
+ border: solid #ddd;
+ border-width: 0px 1px 1px;
+ >div {
+ >div {
+ border: 1px solid #ddd;
+ }
+ &:nth-child(odd) {
+ background-color: $background-color_1;
+ }
+ }
+ }
+
+ .multi-button {
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+ }
+
+ .run-button-disabled {
+ color: lightgrey;
+ }
+
+
+ // Auto Config styles -------------------------
+ .align-center {
+ display: flex;
+ align-items: center;
+ }
+
+ .mb-15 {
+ margin-bottom: 15px;
+ }
+
+ #progress-container {
+ .progress {
+ padding: 0px;
+ margin: auto;
+ }
+ .fa-exclamation-circle {
+ color: red;
+ }
+ }
+
+ // Edit Group styles ----------------------------------------
+ .row-list {
+ >div {
+ &:nth-child(even) {
+ >div {
+ background-color: $background-color_1;
+ }
+ }
+ }
+ }
+
+ i.fa.fa-solid.fa-trash {
+ font-size: 20px;
+ }
+
+ .modal-dialog {
+ width: 700px;
+ }
+
+ .col-title {
+ margin-bottom: 10px;
+ }
+
+
+ // New Site styles ------------------------------------------
+ .microsoft-block{
+ border: 1px solid black;
+
+ ul.list-unstyled{
+ overflow-y: auto;
+ height: 262px;
+ }
+ }
+
+ .fieldset-collapse {
+ border: 2px solid #8A8A8A;
+ position: relative;
+ margin-bottom: 15px;
+ padding: 1em;
+ }
+
+ .arrow-container {
+ margin-top: 40px;
+ margin-bottom: 20px;
+ font-size: 30px;
+ }
+
+ .synchContainer {
+ display: flex;
+ }
+
+ .dates-container {
+ display: flex;
+ flex-direction: column;
+ margin-top: 10px;
+ padding: 0 30px;
+
+ .date-element {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ margin-top: 10px;
+
+ div {
+ min-width: 120px;
+ text-align: right;
+ font-weight: bold;
+ }
+
+ input {
+ padding: 5px;
+ }
+ }
+ }
+
+
+
+
+ @media (max-width: 1200px) {
+ .synchContainer {
+ display: block;
+ }
+ }
+
+ // Config styles -------------------------------------
+ .config-fieldset{
+ border: 2px solid rgba(138, 138, 138, 1);
+ padding: 1em;
+ position: relative;
+
+ legend {
+ display: inline-block;
+ width: auto;
+ margin-bottom: 0;
+ padding: 0.5em;
+ border: 0 none;
+ }
+ }
+
+ .form-child {
+ padding-left: 40px;
+ }
+}
diff --git a/library/src/skins/default/src/sass/modules/tool/microsoft-collaborativedocuments/_microsoft-collaborativedocuments.scss b/library/src/skins/default/src/sass/modules/tool/microsoft-collaborativedocuments/_microsoft-collaborativedocuments.scss
new file mode 100644
index 000000000000..8723bb945254
--- /dev/null
+++ b/library/src/skins/default/src/sass/modules/tool/microsoft-collaborativedocuments/_microsoft-collaborativedocuments.scss
@@ -0,0 +1,150 @@
+.#{$namespace}microsoft-collaborativedocuments {
+
+ $color-hover: var(--button-hover-text-color);
+ $color-active: var(--button-active-text-color);
+
+ $background-color: var(--sakai-background-color-2);
+ $background-color-hover: var(--sakai-background-color-3);
+ $background-color-button-hover: var(--button-hover-background);
+ $background-color-button-active: var(--button-active-background);
+
+ $shadow-button-hover: var(--button-hover-shadow);
+ $shadow-button-active: var(--button-active-shadow);
+
+ $border-color-button: var(--button-border-color);
+ $border-color-hover: var(--button-hover-border-color);
+ $border-color-active: var(--button-active-border-color);
+
+ #loading-container {
+ font-size: 22px;
+ }
+
+ /* Accordion section */
+ /* Section Title */
+ .section-title {
+ .refresh-icon {
+ z-index: 3;
+ translate: -5rem;
+ &:hover, &:focus {
+ background-color: var(--button-hover-background);
+ border-color: var(--button-hover-background);
+
+ }
+ &:active {
+ background-color: var(--button-active-background);
+ border-color: var(--button-active-background);
+ }
+ }
+ }
+
+ /* Team/Folder Container */
+ .breadcrumb-header {
+ border-radius: 4px;
+ padding: 8px 15px;
+ margin-bottom: 20px;
+ background-color: $background-color;
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ width: 100%;
+ position: relative;
+ .breadcrumb {
+ padding: 0 15px;
+ margin-left: 3px;
+ margin-bottom: 0;
+ }
+ .act {
+ margin: 0;
+ >button {
+ margin: 0;
+ }
+ }
+ .dropdown-menu {
+ margin: -5px 0 0 15px;
+ a {
+ text-decoration: none;
+ }
+ }
+ }
+ .table-row {
+ display: flex;
+ align-items: center;
+ border: 1px solid #ddd;
+ padding: 10px;
+ &:nth-of-type(even) {
+ background-color: $background-color;
+ }
+ &:hover, &:focus, &.focused-row {
+ background: $background-color-hover;
+ .dropdown-toggle {
+ display: block;
+ &:hover, &:focus {
+ color: $color-hover;
+ text-decoration: none;
+ background: $background-color-button-hover;
+ border-color: $border-color-hover;
+ box-shadow: $shadow-button-hover;
+ }
+ &:active {
+ outline: 0;
+ color: $color-active;
+ text-decoration: none;
+ background: $background-color-button-active;
+ border-color: $border-color-active;
+ box-shadow: $shadow-button-active;
+ }
+ }
+ }
+ .main-column {
+ display: flex;
+ justify-content: space-between;
+ }
+ .dropdown-toggle {
+ display: none;
+ /*border: 1px solid $border-color-button;*/
+ border-radius: 4px;
+ padding: 0 5px;
+ text-decoration: none;
+ position: relative;
+ .dropdown-menu {
+ position: absolute;
+ margin-top: 5px;
+ }
+ }
+ a {
+ text-decoration: none;
+ }
+ }
+ form.dropzone {
+ margin-top: 10px;
+ }
+
+ /* Login/Logout related items */
+ .menu-container {
+ position: relative;
+ min-height: 40px;
+ display: flex;
+ align-items: flex-end;
+ margin-bottom: 10px;
+ margin-top: 20px;
+ .menu {
+ width: 100%;
+ }
+ .login-header {
+ position: absolute;
+ right: 0;
+ bottom: 7px;
+ .user-account {
+ font-size: smaller;
+ font-weight: bold;
+ }
+ }
+ }
+
+ /* Special margin for small screens */
+ @media (max-width: 800px) {
+ .menu-container {
+ margin-top: 4em;
+ }
+ }
+}
diff --git a/library/src/skins/default/src/sass/modules/tool/microsoft-mediagallery/_microsoft-mediagallery.scss b/library/src/skins/default/src/sass/modules/tool/microsoft-mediagallery/_microsoft-mediagallery.scss
new file mode 100644
index 000000000000..c054af5cea37
--- /dev/null
+++ b/library/src/skins/default/src/sass/modules/tool/microsoft-mediagallery/_microsoft-mediagallery.scss
@@ -0,0 +1,217 @@
+.#{$namespace}microsoft-mediagallery {
+
+ $color_info: #ff000090;
+ $color_info_hover: red;
+ $color_play: #007dff80;
+ $color_play_hover: #0000ff;
+ $color_thumbnail: grey;
+ $background-color_overlay: #00000033;
+ $color_folder: var(--sakai-primary-color-1);
+
+ .container-fluid {
+ margin-left: 0;
+ padding-left: 0;
+ margin-right: 0;
+ padding-right: 0;
+ }
+
+ #loading-container {
+ font-size: 22px;
+ }
+
+ .menu {
+ margin-bottom: 10px;
+ }
+
+ /* Login/Logout related items */
+ .login-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ min-height: 200px;
+ gap: 10px;
+ }
+
+ .logout-header {
+ display: flex;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+
+ .user-account {
+ font-size: smaller;
+ font-weight: bold;
+ }
+ }
+
+ /* Container Header (tree view + sortby + search) */
+ .container-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ margin-bottom: 15px;
+ flex-wrap: wrap;
+
+ .icon-toggle {
+ margin-left: 5px;
+ }
+
+ form {
+ display: flex;
+ align-items: stretch;
+ column-gap: 10px;
+
+ input {
+ height: 100%;
+
+ /* Search */
+ &.input-search {
+ font-family:Arial, FontAwesome;
+ max-width: 300px;
+ padding-left: 5px;
+ }
+ }
+ }
+ }
+
+
+ /* Accordion section */
+ /* Section Title */
+ .section-title {
+ .refresh-icon {
+ z-index: 3;
+ translate: -5rem;
+ &:hover, &:focus {
+ background-color: var(--button-hover-background);
+ border-color: var(--button-hover-background);
+
+ }
+ &:active {
+ background-color: var(--button-active-background);
+ border-color: var(--button-active-background);
+ }
+ }
+ }
+
+ /* Items (video/folder) in rows */
+ .display-flex {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: stretch;
+
+ >.video-item {
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 10px;
+ justify-content: flex-end;
+ }
+ }
+
+ /* Video item */
+ .video-item {
+ .video-content {
+ position: relative;
+ border: thin solid black;
+
+ .info-icon {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ color: $color_info;
+ text-decoration: none;
+ font-size: 20px;
+
+ &:hover {
+ color: $color_info_hover;
+ text-decoration: none;
+ cursor: pointer;
+ }
+ }
+
+ .thumb-overlay {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ background-color: $background-color_overlay;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 40px;
+
+ .play-icon {
+ text-decoration: none;
+ color: $color_play;
+
+ &:hover {
+ color: $color_play_hover;
+ cursor: pointer;
+ text-decoration: none;
+ }
+ }
+ }
+
+ .thumbnail-container {
+ font-size: 60px;
+ color: $color_thumbnail;
+ }
+ }
+ }
+
+ /* Folder Item */
+ .folder-content {
+ font-size: 80px;
+ color: $color_folder;
+
+ .folder-item {
+ color: $color_folder;
+
+ &:hover {
+ scale: 1.2;
+ }
+ }
+
+ }
+
+ /* Inside a folder (shows breadcrumb bar) */
+ .folder-row{
+ .breadcrumb {
+ margin-top: -15px;
+ }
+ }
+
+ /* Info */
+ .info-container {
+ display: flex;
+ justify-content: center;
+ }
+ .item-container {
+ display: flex;
+ flex-direction: column;
+ }
+ .item-property {
+ display: flex;
+ margin-bottom: 5px;
+
+ label {
+ // Override global margin on label
+ margin: unset;
+ margin-right: 5px;
+ }
+
+ .thumbnail-container {
+ font-size: 60px;
+ }
+ }
+
+ /* Special margin for small screens */
+ @media (max-width: 800px) {
+ .container-view {
+ margin-top: 2em;
+ }
+ .logout-header {
+ margin-top: 2em;
+ }
+ }
+}
diff --git a/library/src/skins/default/src/sass/tool.scss b/library/src/skins/default/src/sass/tool.scss
index 9fedf791ce54..565e9f14c5b9 100644
--- a/library/src/skins/default/src/sass/tool.scss
+++ b/library/src/skins/default/src/sass/tool.scss
@@ -168,6 +168,9 @@ $jumbotron-heading-font-size: $h4-font-size;
@import "modules/tool/emailtemplates/emailtemplates";
@import "modules/tool/wiki/wiki";
@import "modules/tool/bulk-user-membership/bulk-user-membership";
+@import "modules/tool/microsoft-admin/microsoft-admin";
+@import "modules/tool/microsoft-mediagallery/microsoft-mediagallery";
+@import "modules/tool/microsoft-collaborativedocuments/microsoft-collaborativedocuments";
@import "base/rtl";
diff --git a/master/pom.xml b/master/pom.xml
index a618fbfedcb9..f8abd73d32d8 100644
--- a/master/pom.xml
+++ b/master/pom.xml
@@ -1912,12 +1912,6 @@
${sakai.version}
provided
-
- org.sakaiproject
- sakai-onedrive-api
- ${sakai.version}
- provided
-
org.sakaiproject
sakai-podcasts-api
@@ -2104,6 +2098,12 @@
${sakai.version}
provided
+
+ org.sakaiproject.microsoft
+ microsoft-api
+ ${sakai.version}
+ provided
+
diff --git a/meetings/README.md b/meetings/README.md
new file mode 100644
index 000000000000..ac7097b6c3b4
--- /dev/null
+++ b/meetings/README.md
@@ -0,0 +1,111 @@
+# SAKAI - Online Meetings tool
+
+This is a tool for Sakai dedicated to the creation, management and use of virtual meeting rooms based on different online video conferencing providers. Instructors can schedule new meetings for their students on a site or group basis. Students can see a list of their scheduled meetings for each site and access them directly by clicking on them. Only members of the organization can access these meetings, unless the organizer allows guest access.
+
+#### Features
+
+- Instructors can schedule new meetings for their students.
+- Instructors can control access to meetings by site or group.
+- Students can access a list of their scheduled meetings.
+- Search for scheduled meetings by name.
+- Meetings can be added as events to the site calendar.
+- Instructors can notify all meeting participants by e-mail.
+- Only members of the organization can access Microsoft Teams meetings.
+- Only the organizer can invite external users to access the Microsoft Teams meeting.
+- Simple, fast and responsive interface.
+
+## Current supported providers
+- Microsoft Teams
+
+## Prerequisites
+You need:
+- A Sakai 22.x instance or higher.
+- For Microsoft Teams integration:
+ - A Microsoft Azure Active Directory application.
+ - Azure Active Directory users must have the same email in Sakai to be identified as members of the organization.
+
+## Microsoft Teams
+### Azure AD configuration
+You must create a new application in the _App Registrations_ section of the Azure Active Directory portal by clicking on the _New Registration_ button.
+
+
+
+You can enter a name and select the supported account types. The _Single tenant_ option is marked by default.
+
+
+
+To grant **Meetings tool** access to your registered Azure application, you will need a **client secret**. To obtain this, you can access the _Certificates & secrets_ section within the configuration page of your registered Azure application.
+
+
+
+Once the app is created, you need to configure the permissions for your registered Azure App in the _API Permissions_ section. To add a new permission you must click _Add a permission_, then select _Microsoft Graph_ and _Application Permissions_.
+
+
+
+The permissions to enable are defined in the following table:
+
+```sh
+Directory.Read.All
+Directory.ReadWrite.All
+Group.Read.All
+OnlineMeetings.ReadWrite.All
+User.Read.All
+-- Chat Messages
+Chat.Read.All
+ChatMessage.Read.All
+-- Create link
+Files.ReadWrite.All
+Sites.ReadWrite.All
+Sites.Manage.All
+```
+
+Then you must click on the _Grant admin consent_ button for your Azure directory.
+
+### Application access directives
+In order for Sakai to manage meetings on its own, without user authentication by Microsoft, you need to set up an application user and its access policies. You can read the official Microsoft documentation on these steps:
+
+https://docs.microsoft.com/en-us/graph/cloud-communication-online-meeting-application-access-policy
+
+#### Summary
+ #Install Microsoft Teams Poweshell Module: https://learn.microsoft.com/en-us/microsoftteams/teams-powershell-install
+
+ #Get version
+ $PSVersionTable.PSVersion
+
+ #Install module
+ Install-Module -Name PowerShellGet -Force -AllowClobber
+ Install-Module -Name MicrosoftTeams -Force -AllowClobber
+
+ #Enable script running
+ Set-ExecutionPolicy Unrestricted
+
+ #Connect to Teams
+ Import-Module MicrosoftTeams
+ $userCredential = Get-Credential
+ Connect-MicrosoftTeams -Credential $userCredential
+
+ #Create policy
+ New-CsApplicationAccessPolicy -Identity Test-policy -AppIds "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -Description "OnlineMeetings Access Policy"
+
+ #Grant to specific user (sakai@nkrd.onmicrosoft.com)
+ Grant-CsApplicationAccessPolicy -PolicyName Test-policy -Identity "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
+
+ #(Optional)Grant Global
+ Grant-CsApplicationAccessPolicy -PolicyName Test-policy -Global
+
+### Access to Protected APIs
+~~To get Microsoft chat messages using application permissions, we need to request access to this protected API: https://learn.microsoft.com/en-us/graph/teams-protected-apis~~
+Request to protected APIs is no longer needed. Everything should work as long as these requirements are accomplished: https://learn.microsoft.com/en-us/graph/auth-v2-service?tabs=http
+
+## Global Microsoft README
+Remember to set all Microsoft related configuration according to: [microsoft-integration/README.md](../microsoft-integration/README.md)
+
+## Future plans and Roadmap
+
+- Improve documentation.
+- Improve Microsoft permissions for institutions using Microsoft accounts.
+- Reuse the meeting card component in other tools like Lessons or create a meeting widget.
+- Support other webconference providers like Zoom or BBB depending on funding or contributions.
+
+## Contact
+If you have any questions please contact the devs at **Entornos de Formacion S.L.** at sakaigers@edf.global
diff --git a/meetings/api/pom.xml b/meetings/api/pom.xml
new file mode 100644
index 000000000000..4d295124fd08
--- /dev/null
+++ b/meetings/api/pom.xml
@@ -0,0 +1,37 @@
+
+
+ 4.0.0
+
+
+ org.sakaiproject.meetings
+ meetings
+ 24-SNAPSHOT
+ ../pom.xml
+
+
+ meetings-api
+ org.sakaiproject.meetings
+ meetings-api
+ jar
+
+
+ shared
+
+
+
+
+
+ org.hibernate
+ hibernate-core
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
+
+ org.sakaiproject.kernel
+ sakai-kernel-api
+
+
+
diff --git a/meetings/api/src/java/org/sakaiproject/meetings/api/MeetingService.java b/meetings/api/src/java/org/sakaiproject/meetings/api/MeetingService.java
new file mode 100644
index 000000000000..4fd980ff2ba8
--- /dev/null
+++ b/meetings/api/src/java/org/sakaiproject/meetings/api/MeetingService.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2024 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.meetings.api;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.sakaiproject.meetings.api.model.Meeting;
+
+public interface MeetingService {
+
+ public Iterable getAllMeetings();
+ public List getAllMeetingsFromSite(String siteId);
+ public List getUserMeetings(String userId, String siteId, List groupIds);
+ public Meeting createMeeting(Meeting meetingData);
+ public void updateMeeting(Meeting meetingData);
+ public void deleteMeetingById(String id);
+ public Optional getMeetingById(String id);
+ public Meeting getMeeting(String id);
+ public void removeSiteAndGroupAttendeesByMeetingId(String id);
+ public void setMeetingProperty(Meeting meeting, String property, String value);
+ public String getMeetingProperty(Meeting meeting, String property);
+ public void removeMeetingProperty(Meeting meeting, String property);
+
+}
diff --git a/meetings/api/src/java/org/sakaiproject/meetings/api/model/AttendeeType.java b/meetings/api/src/java/org/sakaiproject/meetings/api/model/AttendeeType.java
new file mode 100644
index 000000000000..8c89844c6feb
--- /dev/null
+++ b/meetings/api/src/java/org/sakaiproject/meetings/api/model/AttendeeType.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2024 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.meetings.api.model;
+
+public enum AttendeeType {
+ USER,
+ SITE,
+ GROUP
+}
diff --git a/meetings/api/src/java/org/sakaiproject/meetings/api/model/Meeting.java b/meetings/api/src/java/org/sakaiproject/meetings/api/model/Meeting.java
new file mode 100644
index 000000000000..2af25cc37531
--- /dev/null
+++ b/meetings/api/src/java/org/sakaiproject/meetings/api/model/Meeting.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright (c) 2024 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.meetings.api.model;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.Lob;
+import javax.persistence.ManyToOne;
+import javax.persistence.OneToMany;
+import javax.persistence.Table;
+
+import org.hibernate.annotations.GenericGenerator;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIdentityInfo;
+import com.fasterxml.jackson.annotation.ObjectIdGenerators;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "meetings")
+@AllArgsConstructor
+@Data
+@NoArgsConstructor
+@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
+public class Meeting {
+
+ @Id
+ @Column(name = "meeting_id", length = 99, nullable = false)
+ @GeneratedValue(generator = "uuid")
+ @GenericGenerator(name = "uuid", strategy = "uuid2")
+ private String id;
+
+ @Column(name = "meeting_title", length = 255, nullable = false)
+ private String title;
+
+ @Lob
+ @Column(name = "meeting_description", length = 4000)
+ private String description;
+
+ @Column(name = "meeting_site_id", length = 99)
+ private String siteId;
+
+ @Column(name = "meeting_start_date")
+ @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss[.SSS]Z", timezone = "UTC")
+ private Instant startDate;
+
+ @Column(name = "meeting_end_date")
+ @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss[.SSS]Z", timezone = "UTC")
+ private Instant endDate;
+
+ @Column(name = "meeting_url", length = 255)
+ private String url;
+
+ @Column(name = "meeting_owner_id", length = 99)
+ private String ownerId;
+
+ @ManyToOne
+ @JoinColumn(name="meeting_provider_id")
+ private MeetingsProvider provider;
+
+ @OneToMany(mappedBy="meeting", cascade = CascadeType.ALL)
+ private List attendees;
+
+ /**
+ * Extract meeting ID from URL
+ * @return meetingId
+ */
+ public String getMeetingId() {
+ String ret = null;
+ if(!"".equals(this.url)) {
+ Pattern teamPattern = Pattern.compile("^https://teams.microsoft.com/l/meetup-join/([^/]+)/.*$");
+ Matcher matcher = teamPattern.matcher(this.url);
+ if(matcher.find()) {
+ ret = matcher.group(1);
+ }
+ }
+ return ret;
+ }
+}
diff --git a/meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingAttendee.java b/meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingAttendee.java
new file mode 100644
index 000000000000..f73c0c09c601
--- /dev/null
+++ b/meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingAttendee.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2024 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.meetings.api.model;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.SequenceGenerator;
+import javax.persistence.Table;
+
+import com.fasterxml.jackson.annotation.JsonIdentityInfo;
+import com.fasterxml.jackson.annotation.ObjectIdGenerators;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "meeting_attendees")
+@AllArgsConstructor
+@Data
+@NoArgsConstructor
+@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
+public class MeetingAttendee {
+
+ @Id
+ @Column(name = "attendee_id")
+ @GeneratedValue(strategy = GenerationType.AUTO, generator = "meeting_attendee_sequence")
+ @SequenceGenerator(name = "meeting_attendee_sequence", sequenceName = "MEETING_ATTENDEE_S")
+ private Long id;
+
+ @ManyToOne
+ @JoinColumn(name="attendee_meeting_id")
+ private Meeting meeting;
+
+ @Column(name = "attendee_type")
+ private AttendeeType type;
+
+ @Column(name = "attendee_object_id", length = 255)
+ private String objectId;
+
+}
diff --git a/meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingProperty.java b/meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingProperty.java
new file mode 100644
index 000000000000..a4d75abfa8cf
--- /dev/null
+++ b/meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingProperty.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2024 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.meetings.api.model;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.SequenceGenerator;
+import javax.persistence.Table;
+
+import com.fasterxml.jackson.annotation.JsonIdentityInfo;
+import com.fasterxml.jackson.annotation.ObjectIdGenerators;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "meeting_properties")
+@AllArgsConstructor
+@Data
+@NoArgsConstructor
+@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
+public class MeetingProperty {
+
+ @Id
+ @Column(name = "prop_id")
+ @GeneratedValue(strategy = GenerationType.AUTO, generator = "meeting_property_sequence")
+ @SequenceGenerator(name = "meeting_property_sequence", sequenceName = "MEETING_PROPERTY_S")
+ private Long id;
+
+ @ManyToOne(cascade = CascadeType.ALL)
+ @JoinColumn(name="prop_meeting_id")
+ private Meeting meeting;
+
+ @Column(name = "prop_name", length = 255, nullable = false)
+ private String name;
+
+ @Column(name = "prop_value", length = 255)
+ private String value;
+
+}
diff --git a/meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingsProvider.java b/meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingsProvider.java
new file mode 100644
index 000000000000..15da100b04ac
--- /dev/null
+++ b/meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingsProvider.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2024 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.meetings.api.model;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+import org.hibernate.annotations.GenericGenerator;
+
+import com.fasterxml.jackson.annotation.JsonIdentityInfo;
+import com.fasterxml.jackson.annotation.ObjectIdGenerators;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "meeting_providers")
+@AllArgsConstructor
+@Data
+@NoArgsConstructor
+@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
+public class MeetingsProvider {
+
+ @Id
+ @Column(name = "provider_id", length = 99, nullable = false)
+ @GeneratedValue(generator = "uuid")
+ @GenericGenerator(name = "uuid", strategy = "uuid2")
+ private String id;
+
+ @Column(name = "provider_name", length = 255, nullable = false)
+ private String name;
+
+}
diff --git a/meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingAttendeeRepository.java b/meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingAttendeeRepository.java
new file mode 100644
index 000000000000..e33e61251be4
--- /dev/null
+++ b/meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingAttendeeRepository.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2024 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.meetings.api.persistence;
+
+import org.sakaiproject.meetings.api.model.MeetingAttendee;
+import org.sakaiproject.serialization.SerializableRepository;
+
+public interface MeetingAttendeeRepository extends SerializableRepository {
+
+ public void removeAttendeesByMeetingId(String meetingId);
+ public void removeSiteAndGroupAttendeesByMeetingId (String meetingId);
+
+}
diff --git a/meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingPropertyRepository.java b/meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingPropertyRepository.java
new file mode 100644
index 000000000000..6781ade00bfc
--- /dev/null
+++ b/meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingPropertyRepository.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2024 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.meetings.api.persistence;
+
+import java.util.Optional;
+
+import org.sakaiproject.meetings.api.model.MeetingProperty;
+import org.sakaiproject.serialization.SerializableRepository;
+
+public interface MeetingPropertyRepository extends SerializableRepository {
+
+ public Optional findFirstByMeetingIdAndName(String meetingId, String name);
+ public void deletePropertiesByMeetingId(String meetingId);
+ public void deletePropertyByMeetingIdAndName(String meetingId, String propertyName);
+
+}
diff --git a/meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingRepository.java b/meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingRepository.java
new file mode 100644
index 000000000000..90bd47c3c4e4
--- /dev/null
+++ b/meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingRepository.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2024 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.meetings.api.persistence;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.sakaiproject.meetings.api.model.Meeting;
+import org.sakaiproject.serialization.SerializableRepository;
+
+public interface MeetingRepository extends SerializableRepository {
+
+ public Optional findById(String id);
+ public Meeting findMeetingById(String id);
+ public void deleteById(String id);
+ public List getSiteMeetings(String siteId);
+ public List getMeetings(String userId, String siteId, List groupIds);
+
+}
diff --git a/meetings/impl/pom.xml b/meetings/impl/pom.xml
new file mode 100644
index 000000000000..96ef9bfcaa8f
--- /dev/null
+++ b/meetings/impl/pom.xml
@@ -0,0 +1,110 @@
+
+
+ 4.0.0
+
+
+ org.sakaiproject.meetings
+ meetings
+ 24-SNAPSHOT
+ ../pom.xml
+
+
+ meetings-impl
+ org.sakaiproject.meetings
+ meetings-impl
+ sakai-component
+
+
+
+ 5.5.2
+ 2.22.2
+
+
+
+
+ org.sakaiproject.meetings
+ meetings-api
+ ${project.version}
+ provided
+
+
+
+
+ org.sakaiproject.kernel
+ sakai-kernel-api
+
+
+
+
+ org.springframework
+ spring-tx
+
+
+ org.springframework
+ spring-context
+
+
+ org.springframework
+ spring-orm
+
+
+
+
+ org.hibernate
+ hibernate-core
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-xml
+
+
+ org.quartz-scheduler
+ quartz
+
+
+ org.apache.commons
+ commons-lang3
+
+
+
+
+ org.springframework
+ spring-test
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.hsqldb
+ hsqldb
+ test
+
+
+ javax.servlet
+ javax.servlet-api
+ test
+
+
+
+
+
+ ${basedir}/src/main/java
+ ${basedir}/src/test/java
+
+
+ ${basedir}/src/webapp
+
+
+ ${basedir}/src/test/resources
+
+
+
+
diff --git a/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/MeetingServiceImpl.java b/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/MeetingServiceImpl.java
new file mode 100644
index 000000000000..278aadfa2e97
--- /dev/null
+++ b/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/MeetingServiceImpl.java
@@ -0,0 +1,117 @@
+/**
+ * Copyright (c) 2024 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.meetings.impl;
+
+import org.springframework.transaction.annotation.Transactional;
+import org.sakaiproject.meetings.api.model.Meeting;
+import org.sakaiproject.meetings.api.model.MeetingProperty;
+import org.sakaiproject.meetings.api.persistence.MeetingAttendeeRepository;
+import org.sakaiproject.meetings.api.persistence.MeetingPropertyRepository;
+import org.sakaiproject.meetings.api.persistence.MeetingRepository;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.sakaiproject.meetings.api.MeetingService;
+
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Transactional
+public class MeetingServiceImpl implements MeetingService {
+
+ @Setter
+ MeetingRepository meetingRepository;
+
+ @Setter
+ MeetingPropertyRepository meetingPropertyRepository;
+
+ @Setter
+ MeetingAttendeeRepository meetingAttendeeRepository;
+
+ public void init() {
+ log.info("Initializing Meeting Service");
+ }
+
+ public Iterable getAllMeetings() {
+ return meetingRepository.findAll();
+ }
+
+ public List getAllMeetingsFromSite(String siteId) {
+ return meetingRepository.getSiteMeetings(siteId);
+ }
+
+ public List getUserMeetings(String userId, String siteId, List groupIds) {
+ return meetingRepository.getMeetings(userId, siteId, groupIds);
+ }
+
+ public Optional getMeetingById(String id) {
+ return meetingRepository.findById(id);
+ }
+
+ public Meeting getMeeting(String id) {
+ return meetingRepository.findMeetingById(id);
+ }
+
+ public Meeting createMeeting(Meeting meetingData) {
+ return meetingRepository.save(meetingData);
+ }
+
+ public void updateMeeting(Meeting meetingData) {
+ meetingRepository.update(meetingData);
+ }
+
+ public void deleteMeetingById(String id) {
+ meetingPropertyRepository.deletePropertiesByMeetingId(id);
+ meetingAttendeeRepository.removeAttendeesByMeetingId(id);
+ meetingRepository.deleteById(id);
+ }
+
+ public void removeSiteAndGroupAttendeesByMeetingId(String id) {
+ meetingAttendeeRepository.removeSiteAndGroupAttendeesByMeetingId(id);
+ }
+
+ public void setMeetingProperty(Meeting meeting, String property, String value) {
+ Optional optMeetingProp = meetingPropertyRepository.findFirstByMeetingIdAndName(meeting.getId(), property);
+ if (optMeetingProp.isPresent()) {
+ MeetingProperty meetingProp = optMeetingProp.get();
+ meetingProp.setValue(value);
+ meetingPropertyRepository.update(meetingProp);
+ } else {
+ MeetingProperty meetingProp = new MeetingProperty();
+ meetingProp.setMeeting(meeting);
+ meetingProp.setName(property);
+ meetingProp.setValue(value);
+ meetingPropertyRepository.save(meetingProp);
+ }
+ }
+
+ public String getMeetingProperty(Meeting meeting, String property) {
+ String result = null;
+ Optional optMeetingProp = meetingPropertyRepository.findFirstByMeetingIdAndName(meeting.getId(), property);
+ if (optMeetingProp.isPresent()) {
+ MeetingProperty prop = optMeetingProp.get();
+ result = prop.getValue();
+ }
+ return result;
+ }
+
+ public void removeMeetingProperty(Meeting meeting, String property) {
+ meetingPropertyRepository.deletePropertyByMeetingIdAndName(meeting.getId(), property);
+ }
+
+}
diff --git a/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingAttendeeRepositoryImpl.java b/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingAttendeeRepositoryImpl.java
new file mode 100644
index 000000000000..276aacd99796
--- /dev/null
+++ b/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingAttendeeRepositoryImpl.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2024 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.meetings.impl.persistence;
+
+import org.hibernate.Session;
+import org.sakaiproject.meetings.api.model.MeetingAttendee;
+import org.sakaiproject.meetings.api.persistence.MeetingAttendeeRepository;
+import org.sakaiproject.serialization.BasicSerializableRepository;
+
+public class MeetingAttendeeRepositoryImpl extends BasicSerializableRepository implements MeetingAttendeeRepository{
+
+ public Session getCurrentSession() {
+ return sessionFactory.getCurrentSession();
+ }
+
+ @Override
+ public void removeAttendeesByMeetingId(String meetingId) {
+ getCurrentSession().createQuery("delete from MeetingAttendee where meeting.id = :id").setParameter("id", meetingId).executeUpdate();
+ }
+
+ @Override
+ public void removeSiteAndGroupAttendeesByMeetingId (String meetingId) {
+ getCurrentSession().createQuery("delete from MeetingAttendee where meeting.id = :id and type in (1, 2)").setParameter("id", meetingId).executeUpdate();
+ }
+
+}
diff --git a/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingPropertyRepositoryImpl.java b/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingPropertyRepositoryImpl.java
new file mode 100644
index 000000000000..7c338a918a09
--- /dev/null
+++ b/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingPropertyRepositoryImpl.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2024 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.meetings.impl.persistence;
+
+import java.util.Optional;
+
+import org.hibernate.Session;
+import org.sakaiproject.meetings.api.model.MeetingProperty;
+import org.sakaiproject.meetings.api.persistence.MeetingPropertyRepository;
+import org.sakaiproject.serialization.BasicSerializableRepository;
+
+public class MeetingPropertyRepositoryImpl extends BasicSerializableRepository implements MeetingPropertyRepository{
+
+ public Session getCurrentSession() {
+ return sessionFactory.getCurrentSession();
+ }
+
+ @Override
+ public Optional findFirstByMeetingIdAndName(String meetingId, String name) {
+ MeetingProperty result = (MeetingProperty) getCurrentSession()
+ .createQuery("from MeetingProperty where meeting.id = :id and name = :name")
+ .setParameter("id", meetingId)
+ .setParameter("name", name).uniqueResult();
+ return Optional.ofNullable(result);
+ }
+
+ @Override
+ public void deletePropertiesByMeetingId(String meetingId) {
+ getCurrentSession().createQuery("delete from MeetingProperty where meeting.id = :id").setParameter("id", meetingId).executeUpdate();
+ }
+
+ @Override
+ public void deletePropertyByMeetingIdAndName(String meetingId, String propertyName) {
+ getCurrentSession().createQuery("delete from MeetingProperty where meeting.id = :id and name = :name")
+ .setParameter("id", meetingId)
+ .setParameter("name", propertyName)
+ .executeUpdate();
+ }
+
+}
diff --git a/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingRepositoryImpl.java b/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingRepositoryImpl.java
new file mode 100644
index 000000000000..6fe3f307a495
--- /dev/null
+++ b/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingRepositoryImpl.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright (c) 2024 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.meetings.impl.persistence;
+
+import org.sakaiproject.meetings.api.persistence.MeetingRepository;
+import org.sakaiproject.serialization.BasicSerializableRepository;
+import org.springframework.util.CollectionUtils;
+
+import java.util.List;
+import java.util.Optional;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Join;
+import javax.persistence.criteria.Predicate;
+import javax.persistence.criteria.Root;
+
+import org.hibernate.Session;
+import org.hibernate.criterion.Restrictions;
+import org.sakaiproject.meetings.api.model.AttendeeType;
+import org.sakaiproject.meetings.api.model.Meeting;
+import org.sakaiproject.meetings.api.model.MeetingAttendee;
+
+public class MeetingRepositoryImpl extends BasicSerializableRepository implements MeetingRepository {
+
+ public Session getCurrentSession() {
+ return sessionFactory.getCurrentSession();
+ }
+
+ public Optional findById(String id) {
+ Meeting meeting = (Meeting) startCriteriaQuery().add(Restrictions.eq("id", id)).uniqueResult();
+ return Optional.ofNullable(meeting);
+ }
+
+ public Meeting findMeetingById(String id) {
+ return (Meeting) startCriteriaQuery().add(Restrictions.eq("id", id)).uniqueResult();
+ }
+
+ @Override
+ public void deleteById(String id) {
+ getCurrentSession().createQuery("delete from Meeting where id = :id").setParameter("id", id).executeUpdate();
+ }
+
+ @Override
+ public List getSiteMeetings(String siteId) {
+ CriteriaBuilder criteriaBuilder = getCurrentSession().getCriteriaBuilder();
+ CriteriaQuery query = criteriaBuilder.createQuery(Meeting.class);
+ Root root = query.from(Meeting.class);
+ Predicate siteRestriction = criteriaBuilder.equal(root.get("siteId"), siteId);
+ query.select(root).where(siteRestriction).distinct(true);
+ return getCurrentSession().createQuery(query).getResultList();
+ }
+
+ @Override
+ public List getMeetings(String userId, String siteId, List groupIds) {
+ CriteriaBuilder criteriaBuilder = getCurrentSession().getCriteriaBuilder();
+ CriteriaQuery query = criteriaBuilder.createQuery(Meeting.class);
+ Root root = query.from(Meeting.class);
+ Join joinAttendees = root.join("attendees");
+ Predicate orClause = criteriaBuilder.disjunction();
+ if (userId != null) {
+ Predicate userRestriction = criteriaBuilder.and(
+ criteriaBuilder.equal(joinAttendees.get("type"), AttendeeType.USER),
+ criteriaBuilder.equal(joinAttendees.get("objectId"), userId));
+ orClause.getExpressions().add(userRestriction);
+ }
+ if (siteId != null) {
+ Predicate siteRestriction = criteriaBuilder.and(
+ criteriaBuilder.equal(joinAttendees.get("type"), AttendeeType.SITE),
+ criteriaBuilder.equal(joinAttendees.get("objectId"), siteId));
+ orClause.getExpressions().add(siteRestriction);
+ }
+ if (!CollectionUtils.isEmpty(groupIds)) {
+ Predicate groupRestriction = criteriaBuilder.and(
+ criteriaBuilder.equal(joinAttendees.get("type"), AttendeeType.GROUP),
+ joinAttendees.get("objectId").in(groupIds));
+ orClause.getExpressions().add(groupRestriction);
+ }
+ Predicate where = criteriaBuilder.and(criteriaBuilder.equal(root.get("siteId"), siteId), orClause);
+ query.select(root).where(where).distinct(true);
+ return getCurrentSession().createQuery(query).getResultList();
+ }
+
+}
diff --git a/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/MeetingServiceImplTest.java b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/MeetingServiceImplTest.java
new file mode 100644
index 000000000000..301da6718078
--- /dev/null
+++ b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/MeetingServiceImplTest.java
@@ -0,0 +1,252 @@
+/**
+ * Copyright (c) 2024 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.meetings.impl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.sakaiproject.meetings.api.MeetingService;
+import org.sakaiproject.meetings.api.model.AttendeeType;
+import org.sakaiproject.meetings.api.model.Meeting;
+import org.sakaiproject.meetings.api.model.MeetingAttendee;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(classes = {MeetingsImplTestConfiguration.class})
+public class MeetingServiceImplTest extends AbstractTransactionalJUnit4SpringContextTests {
+
+ @Autowired private MeetingService meetingService;
+
+ @Before
+ public void setUp() {
+ // Meeting "Test 0" - SITE1 - Perms: GROUP1
+ Meeting data = new Meeting();
+ data.setTitle("Test 0");
+ data.setDescription("Test 0");
+ data.setSiteId("site1");
+ // Attendees
+ List attendees = new ArrayList();
+ // Attendee 1 - GROUP
+ MeetingAttendee attendee = new MeetingAttendee();
+ attendee.setObjectId("groupId1");
+ attendee.setType(AttendeeType.GROUP);
+ attendees.add(attendee);
+ attendee.setMeeting(data);
+ // Create meeting 1
+ data.setAttendees(attendees);
+ data = meetingService.createMeeting(data);
+
+ // Meeting "Test 1" - SITE1 - Perms: USERID1, GROUP1, GROUP2
+ data = new Meeting();
+ data.setTitle("Test 1");
+ data.setDescription("Test 1");
+ data.setSiteId("site1");
+ // Attendees
+ attendees = new ArrayList();
+ // Attendee 1 - USER
+ attendee = new MeetingAttendee();
+ attendee.setObjectId("userId1");
+ attendee.setType(AttendeeType.USER);
+ attendees.add(attendee);
+ attendee.setMeeting(data);
+ // Attendee 2 - GROUP
+ attendee = new MeetingAttendee();
+ attendee.setObjectId("groupId1");
+ attendee.setType(AttendeeType.GROUP);
+ attendees.add(attendee);
+ attendee.setMeeting(data);
+ // Attendee 3 - GROUP
+ attendee = new MeetingAttendee();
+ attendee.setObjectId("groupId2");
+ attendee.setType(AttendeeType.GROUP);
+ attendees.add(attendee);
+ attendee.setMeeting(data);
+ // Create meeting 1
+ data.setAttendees(attendees);
+ data = meetingService.createMeeting(data);
+
+ // Meeting "Test 2" - SITE2 - Perms: USERID2, SITE2
+ data = new Meeting();
+ data.setTitle("Test 2");
+ data.setDescription("Test 2");
+ data.setSiteId("site2");
+ // Attendees
+ attendees = new ArrayList();
+ // Attendee 1 - USER
+ attendee = new MeetingAttendee();
+ attendee.setObjectId("userId2");
+ attendee.setType(AttendeeType.USER);
+ attendees.add(attendee);
+ attendee.setMeeting(data);
+ // Attendee 2 - SITE
+ attendee = new MeetingAttendee();
+ attendee.setObjectId("site2");
+ attendee.setType(AttendeeType.SITE);
+ attendees.add(attendee);
+ attendee.setMeeting(data);
+ // Create meeting 2
+ data.setAttendees(attendees);
+ data = meetingService.createMeeting(data);
+
+ // Meeting "Test 3" - SITE2 - Perms: USERID2
+ data = new Meeting();
+ data.setTitle("Test 3");
+ data.setDescription("Test 3");
+ data.setSiteId("site3");
+ // Attendees
+ attendees = new ArrayList();
+ // Attendee 1 - USER
+ attendee = new MeetingAttendee();
+ attendee.setObjectId("userId2");
+ attendee.setType(AttendeeType.USER);
+ attendees.add(attendee);
+ attendee.setMeeting(data);
+ // Create meeting 3
+ data.setAttendees(attendees);
+ data = meetingService.createMeeting(data);
+ }
+
+ @Test
+ public void createAndUpdateMeetingTest() {
+ Meeting data = new Meeting();
+ data.setTitle("Test");
+ data.setDescription("Test");
+ data.setSiteId("site");
+ data = meetingService.createMeeting(data);
+ Assert.assertNotNull(data.getId());
+ data.setTitle("Modified");
+ meetingService.updateMeeting(data);
+ Meeting data2 = meetingService.getMeeting(data.getId());
+ Assert.assertTrue("Modified".equals(data2.getTitle()));
+ }
+
+ @Test
+ public void createLongDescriptionMeetingTest() {
+ Meeting data = new Meeting();
+ data.setTitle("Test Long");
+ data.setDescription(RandomStringUtils.randomAlphabetic(4000));
+ data.setSiteId("site");
+ Meeting ret = meetingService.createMeeting(data);
+ Assert.assertNotNull(ret.getId());
+
+ Meeting test = meetingService.getMeeting(ret.getId());
+ Assert.assertNotNull(test.getId());
+
+ Assert.assertThrows(Exception.class,
+ ()->{
+ Meeting data2 = new Meeting();
+ data2.setTitle("Test Long 2");
+ data2.setDescription(RandomStringUtils.randomAlphabetic(4001));
+ data2.setSiteId("site");
+
+ Meeting ret2 = meetingService.createMeeting(data2);
+ Assert.assertNotNull(ret.getId());
+
+ Meeting test2 = meetingService.getMeeting(ret2.getId());
+ //should never reach this point
+ Assert.assertNull(test2.getId());
+ });
+ }
+
+ @Test
+ public void testGetOptionalMeetingByIdIsNotPresent() {
+ Optional optMeeting = meetingService.getMeetingById("nonExistentId");
+ Assert.assertFalse(optMeeting.isPresent());
+ }
+
+ @Test
+ public void testGetOptionalMeetingByIdIsPresent() {
+ Meeting data = new Meeting();
+ data.setTitle("Test");
+ data.setDescription("Test");
+ data.setSiteId("site");
+ data = meetingService.createMeeting(data);
+ Optional optMeeting = meetingService.getMeetingById(data.getId());
+ Assert.assertTrue(optMeeting.isPresent());
+ }
+
+ @Test
+ public void getAllMeetings() {
+ Iterable meetings = meetingService.getAllMeetings();
+ List list = StreamSupport.stream(
+ Spliterators.spliteratorUnknownSize(meetings.iterator(), Spliterator.ORDERED), false)
+ .collect(Collectors.toList());
+ Assert.assertTrue(list.size() == 4);
+ }
+
+ @Test
+ public void getAllMeetingsFromSite() {
+ List list = meetingService.getAllMeetingsFromSite("site2");
+ Assert.assertTrue(list.size() == 1);
+ }
+
+ @Test
+ public void getUserMeetingsUserPermission() {
+ List list = meetingService.getUserMeetings("userId2", "site3", null);
+ Assert.assertTrue(list.size() == 1);
+ }
+
+ @Test
+ public void getUserMeetingsSitePermission() {
+ List list = meetingService.getUserMeetings(null, "site2", null);
+ Assert.assertTrue(list.size() == 1);
+ }
+
+ @Test
+ public void getUserMeetingsGroupPermission() {
+ List list = meetingService.getUserMeetings(null, "site1", Arrays.asList("groupId1"));
+ Assert.assertTrue(list.size() == 2);
+ list = meetingService.getUserMeetings(null, "site1", Arrays.asList("groupId2"));
+ Assert.assertTrue(list.size() == 1);
+ }
+
+ @Test
+ public void deleteMeetingById() {
+ List list = meetingService.getAllMeetingsFromSite("site2");
+ Meeting meeting = list.get(0);
+ String idMeeting = meeting.getId();
+ meetingService.deleteMeetingById(idMeeting);
+ Optional optMeeting = meetingService.getMeetingById(idMeeting);
+ Assert.assertFalse(optMeeting.isPresent());
+ }
+
+ @Test
+ public void setGetAndRemoveMeetingProperty() {
+ List list = meetingService.getAllMeetingsFromSite("site2");
+ Meeting meeting = list.get(0);
+ meetingService.setMeetingProperty(meeting, "property", "value");
+ String value = meetingService.getMeetingProperty(meeting, "property");
+ Assert.assertTrue("value".equals(value));
+ meetingService.removeMeetingProperty(meeting, "property");
+ value = meetingService.getMeetingProperty(meeting, "property");
+ Assert.assertNull(value);
+ }
+
+}
diff --git a/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/MeetingsImplTestConfiguration.java b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/MeetingsImplTestConfiguration.java
new file mode 100644
index 000000000000..0139907a1743
--- /dev/null
+++ b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/MeetingsImplTestConfiguration.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) 2024 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.meetings.impl;
+
+import static org.mockito.Mockito.mock;
+
+import java.io.IOException;
+import java.util.Properties;
+import javax.annotation.Resource;
+import javax.sql.DataSource;
+
+import org.hibernate.boot.registry.StandardServiceRegistry;
+import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
+import org.hibernate.dialect.HSQLDialect;
+import org.hibernate.id.factory.internal.MutableIdentifierGeneratorFactoryInitiator;
+import org.hibernate.SessionFactory;
+import org.hsqldb.jdbcDriver;
+import org.sakaiproject.hibernate.AssignableUUIDGenerator;
+import org.sakaiproject.springframework.orm.hibernate.AdditionalHibernateMappings;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.ImportResource;
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.core.env.Environment;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+import org.springframework.orm.hibernate5.HibernateTransactionManager;
+import org.springframework.orm.hibernate5.LocalSessionFactoryBuilder;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+import org.springframework.transaction.support.TransactionTemplate;
+
+@Configuration
+@EnableTransactionManagement
+@ImportResource("classpath:/WEB-INF/components.xml")
+@PropertySource("classpath:/hibernate.properties")
+public class MeetingsImplTestConfiguration {
+
+ @Autowired
+ private Environment environment;
+
+ @Resource(name = "org.sakaiproject.springframework.orm.hibernate.impl.AdditionalHibernateMappings.meetingstool")
+ private AdditionalHibernateMappings hibernateMappings;
+
+ @Bean(name = "org.sakaiproject.springframework.orm.hibernate.GlobalSessionFactory")
+ public SessionFactory sessionFactory() throws IOException {
+ DataSource dataSource = dataSource();
+ LocalSessionFactoryBuilder sfb = new LocalSessionFactoryBuilder(dataSource);
+ StandardServiceRegistryBuilder srb = sfb.getStandardServiceRegistryBuilder();
+ srb.applySetting(org.hibernate.cfg.Environment.DATASOURCE, dataSource);
+ srb.applySettings(hibernateProperties());
+ StandardServiceRegistry sr = srb.build();
+ sr.getService(MutableIdentifierGeneratorFactoryInitiator.INSTANCE.getServiceInitiated())
+ .register("uuid2", AssignableUUIDGenerator.class);
+ hibernateMappings.processAdditionalMappings(sfb);
+ return sfb.buildSessionFactory(sr);
+ }
+
+ @Bean(name = "javax.sql.DataSource")
+ public DataSource dataSource() {
+ DriverManagerDataSource db = new DriverManagerDataSource();
+ db.setDriverClassName(environment.getProperty(org.hibernate.cfg.Environment.DRIVER, jdbcDriver.class.getName()));
+ db.setUrl(environment.getProperty(org.hibernate.cfg.Environment.URL, "jdbc:hsqldb:mem:test"));
+ db.setUsername(environment.getProperty(org.hibernate.cfg.Environment.USER, "sa"));
+ db.setPassword(environment.getProperty(org.hibernate.cfg.Environment.PASS, ""));
+ return db;
+ }
+
+ @Bean
+ public Properties hibernateProperties() {
+ return new Properties() {
+ {
+ setProperty(org.hibernate.cfg.Environment.DIALECT, environment.getProperty(org.hibernate.cfg.Environment.DIALECT, HSQLDialect.class.getName()));
+ setProperty(org.hibernate.cfg.Environment.HBM2DDL_AUTO, environment.getProperty(org.hibernate.cfg.Environment.HBM2DDL_AUTO));
+ setProperty(org.hibernate.cfg.Environment.ENABLE_LAZY_LOAD_NO_TRANS, environment.getProperty(org.hibernate.cfg.Environment.ENABLE_LAZY_LOAD_NO_TRANS, "true"));
+ setProperty(org.hibernate.cfg.Environment.USE_SECOND_LEVEL_CACHE, environment.getProperty(org.hibernate.cfg.Environment.USE_SECOND_LEVEL_CACHE));
+ setProperty(org.hibernate.cfg.Environment.CURRENT_SESSION_CONTEXT_CLASS, environment.getProperty(org.hibernate.cfg.Environment.CURRENT_SESSION_CONTEXT_CLASS));
+ }
+ };
+ }
+
+ @Bean(name = "org.sakaiproject.springframework.orm.hibernate.GlobalTransactionManager")
+ public HibernateTransactionManager transactionManager(SessionFactory sessionFactory) {
+ HibernateTransactionManager txManager = new HibernateTransactionManager();
+ txManager.setSessionFactory(sessionFactory);
+ return txManager;
+ }
+
+ @Bean(name = "org.springproject.transaction.support.TransactionTemplate")
+ public TransactionTemplate transactionTemplate() {
+ return mock(TransactionTemplate.class);
+ }
+
+}
diff --git a/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingAttendeeRepositoryImplTest.java b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingAttendeeRepositoryImplTest.java
new file mode 100644
index 000000000000..476f0319c7f9
--- /dev/null
+++ b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingAttendeeRepositoryImplTest.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2024 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.meetings.impl.persistence;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.sakaiproject.meetings.api.model.AttendeeType;
+import org.sakaiproject.meetings.api.model.Meeting;
+import org.sakaiproject.meetings.api.model.MeetingAttendee;
+import org.sakaiproject.meetings.api.model.MeetingProperty;
+import org.sakaiproject.meetings.api.persistence.MeetingAttendeeRepository;
+import org.sakaiproject.meetings.api.persistence.MeetingRepository;
+import org.sakaiproject.meetings.impl.MeetingsImplTestConfiguration;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(classes = {MeetingsImplTestConfiguration.class})
+public class MeetingAttendeeRepositoryImplTest {
+
+ @Autowired private MeetingRepository meetingRepository;
+ @Autowired private MeetingAttendeeRepository meetingAttendeeRepository;
+
+ private Meeting meeting;
+
+ @Before
+ public void setUp() {
+ Meeting data = new Meeting();
+ data.setTitle("Test 0");
+ data.setDescription("Test 0");
+ data.setSiteId("site");
+ List attendees = new ArrayList();
+ MeetingAttendee attendee = new MeetingAttendee();
+ attendee.setObjectId("userId");
+ attendee.setType(AttendeeType.USER);
+ attendees.add(attendee);
+ attendee.setMeeting(data);
+ attendee = new MeetingAttendee();
+ attendee.setObjectId("groupId");
+ attendee.setType(AttendeeType.GROUP);
+ attendees.add(attendee);
+ attendee.setMeeting(data);
+ data.setAttendees(attendees);
+ this.meeting = meetingRepository.save(data);
+ }
+
+ @Test
+ public void removeAttendeesByMeetingId() {
+ meetingAttendeeRepository.removeAttendeesByMeetingId(meeting.getId());
+ Meeting meeting = meetingRepository.findMeetingById(this.meeting.getId());
+ Assert.assertEquals(meeting.getAttendees().size(), 0);
+ }
+
+ @Test
+ public void removeSiteAndGroupAttendeesByMeetingId () {
+ meetingAttendeeRepository.removeSiteAndGroupAttendeesByMeetingId(meeting.getId());
+ Meeting meeting = meetingRepository.findMeetingById(this.meeting.getId());
+ Assert.assertEquals(meeting.getAttendees().size(), 1);
+ }
+
+}
diff --git a/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingPropertyRepositoryImplTest.java b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingPropertyRepositoryImplTest.java
new file mode 100644
index 000000000000..4ff163774c8b
--- /dev/null
+++ b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingPropertyRepositoryImplTest.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2024 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.meetings.impl.persistence;
+
+import java.util.Optional;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.sakaiproject.meetings.api.model.Meeting;
+import org.sakaiproject.meetings.api.model.MeetingProperty;
+import org.sakaiproject.meetings.api.persistence.MeetingPropertyRepository;
+import org.sakaiproject.meetings.api.persistence.MeetingRepository;
+import org.sakaiproject.meetings.impl.MeetingsImplTestConfiguration;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(classes = {MeetingsImplTestConfiguration.class})
+public class MeetingPropertyRepositoryImplTest {
+
+ @Autowired private MeetingRepository meetingRepository;
+ @Autowired private MeetingPropertyRepository meetingPropertyRepository;
+
+ private Meeting meeting;
+
+ @Before
+ public void setUp() {
+ Meeting meeting = new Meeting();
+ meeting.setTitle("Test 0");
+ meeting.setDescription("Test 0");
+ meeting.setSiteId("site1");
+ meeting = meetingRepository.save(meeting);
+ MeetingProperty property = new MeetingProperty();
+ property.setName("property");
+ property.setValue("value");
+ property.setMeeting(meeting);
+ meetingPropertyRepository.save(property);
+ this.meeting = meeting;
+ }
+
+ @Test
+ public void findFirstByMeetingIdAndName() {
+ Optional opt = meetingPropertyRepository.findFirstByMeetingIdAndName(meeting.getId(), "property");
+ Assert.assertTrue(opt.isPresent());
+ MeetingProperty prop = opt.get();
+ Assert.assertEquals(prop.getValue(), "value");
+ }
+
+ @Test
+ public void deletePropertiesByMeetingId() {
+ meetingPropertyRepository.deletePropertiesByMeetingId(meeting.getId());
+ Optional opt = meetingPropertyRepository.findFirstByMeetingIdAndName(meeting.getId(), "property");
+ Assert.assertFalse(opt.isPresent());
+ }
+
+}
diff --git a/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingRepositoryImplTest.java b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingRepositoryImplTest.java
new file mode 100644
index 000000000000..3e82460c2c9a
--- /dev/null
+++ b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingRepositoryImplTest.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright (c) 2024 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.meetings.impl.persistence;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.sakaiproject.meetings.api.model.AttendeeType;
+import org.sakaiproject.meetings.api.model.Meeting;
+import org.sakaiproject.meetings.api.model.MeetingAttendee;
+import org.sakaiproject.meetings.api.persistence.MeetingRepository;
+import org.sakaiproject.meetings.impl.MeetingsImplTestConfiguration;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(classes = {MeetingsImplTestConfiguration.class})
+public class MeetingRepositoryImplTest {
+
+ @Autowired private MeetingRepository meetingRepository;
+
+ private Meeting meeting1;
+
+ @Before
+ public void setUp() {
+ // Meeting "Test 0" - SITE3 - Perms: GROUP1
+ Meeting data = new Meeting();
+ data.setTitle("Test 0");
+ data.setDescription("Test 0");
+ data.setSiteId("site3");
+ // Attendees
+ List attendees = new ArrayList();
+ // Attendee 1 - GROUP
+ MeetingAttendee attendee = new MeetingAttendee();
+ attendee.setObjectId("groupId1");
+ attendee.setType(AttendeeType.GROUP);
+ attendees.add(attendee);
+ attendee.setMeeting(data);
+ // Create meeting
+ data.setAttendees(attendees);
+ this.meeting1 = meetingRepository.save(data);
+
+ // Meeting "Test 1" - SITE4 - Perms: USER1
+ data = new Meeting();
+ data.setTitle("Test 1");
+ data.setDescription("Test 1");
+ data.setSiteId("site4");
+ // Attendees
+ attendees = new ArrayList();
+ // Attendee 1 - USER
+ attendee = new MeetingAttendee();
+ attendee.setObjectId("userId");
+ attendee.setType(AttendeeType.USER);
+ attendees.add(attendee);
+ attendee.setMeeting(data);
+ // Create meeting
+ data.setAttendees(attendees);
+ meetingRepository.save(data);
+ }
+
+ @Test
+ public void findById() {
+ Optional meeting = meetingRepository.findById(meeting1.getId());
+ Assert.assertTrue(meeting.isPresent());
+ }
+
+ @Test
+ public void findMeetingById() {
+ Meeting meeting = meetingRepository.findMeetingById(meeting1.getId());
+ Assert.assertNotNull(meeting);
+ }
+
+ @Test
+ public void deleteById() {
+ Meeting meeting = meetingRepository.findMeetingById(meeting1.getId());
+ meetingRepository.delete(meeting);
+ meeting = meetingRepository.findMeetingById(meeting1.getId());
+ Assert.assertNull(meeting);
+ }
+
+ @Test
+ public void getSiteMeetings() {
+ List meetings = meetingRepository.getSiteMeetings("site3");
+ Assert.assertTrue(meetings.size() > 0);
+ }
+
+ @Test
+ public void getMeetingsByUser() {
+ List meetings = meetingRepository.getMeetings("userId", "site3", null);
+ Assert.assertTrue(meetings.size() == 0);
+ meetings = meetingRepository.getMeetings("userId", "site4", null);
+ Assert.assertTrue(meetings.size() > 0);
+ }
+
+}
diff --git a/meetings/impl/src/test/resources/hibernate.properties b/meetings/impl/src/test/resources/hibernate.properties
new file mode 100644
index 000000000000..a1cb68123384
--- /dev/null
+++ b/meetings/impl/src/test/resources/hibernate.properties
@@ -0,0 +1,14 @@
+# Base Hibernate settings
+hibernate.show_sql=false
+hibernate.hbm2ddl.auto=create-only
+hibernate.enable_lazy_load_no_trans=true
+hibernate.cache.use_second_level_cache=false
+hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext
+
+# Connection definition to the HSQLDB database
+hibernate.connection.driver_class=org.hsqldb.jdbcDriver
+hibernate.connection.url=jdbc:hsqldb:mem:test
+hibernate.dialect=org.hibernate.dialect.HSQLDialect
+hibernate.connection.username=sa
+hibernate.connection.password=
+
diff --git a/meetings/impl/src/webapp/WEB-INF/components.xml b/meetings/impl/src/webapp/WEB-INF/components.xml
new file mode 100644
index 000000000000..7162c7b4163f
--- /dev/null
+++ b/meetings/impl/src/webapp/WEB-INF/components.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+ org.sakaiproject.meetings.api.model.Meeting
+ org.sakaiproject.meetings.api.model.MeetingProperty
+ org.sakaiproject.meetings.api.model.MeetingAttendee
+ org.sakaiproject.meetings.api.model.MeetingsProvider
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/meetings/pom.xml b/meetings/pom.xml
new file mode 100644
index 000000000000..51df36f92bad
--- /dev/null
+++ b/meetings/pom.xml
@@ -0,0 +1,24 @@
+
+
+ 4.0.0
+
+ org.sakaiproject
+ master
+ 24-SNAPSHOT
+ ../master/pom.xml
+
+
+ meetings
+ org.sakaiproject.meetings
+ meetings
+ pom
+
+
+ api
+ impl
+ tool
+ ui
+
+
+
diff --git a/cloud-storage/onedrive/tool/pom.xml b/meetings/tool/pom.xml
similarity index 66%
rename from cloud-storage/onedrive/tool/pom.xml
rename to meetings/tool/pom.xml
index 7946847877ae..e3d42e5091bc 100644
--- a/cloud-storage/onedrive/tool/pom.xml
+++ b/meetings/tool/pom.xml
@@ -4,22 +4,46 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
- org.sakaiproject.onedrive
- sakai-onedrive
+ org.sakaiproject.meetings
+ meetings
24-SNAPSHOT
../pom.xml
- org.sakaiproject.onedrive
- sakai-onedrive-tool
- Sakai OneDrive Integration - TOOL
+ org.sakaiproject.meetings
+ meetings-tool
war
-
- true
- true
-
-
+
+ org.sakaiproject.meetings
+ meetings-api
+ ${project.version}
+ provided
+
+
+
+
+ org.sakaiproject.kernel
+ sakai-kernel-api
+ provided
+
+
+ org.sakaiproject.kernel
+ sakai-component-manager
+ provided
+
+
+ org.sakaiproject.kernel
+ sakai-kernel-util
+
+
+ org.sakaiproject.microsoft
+ microsoft-api
+ ${project.version}
+ provided
+
+
+
org.springframework
spring-core
@@ -37,6 +61,7 @@
javax.inject
javax.inject
+
org.thymeleaf
@@ -46,44 +71,47 @@
org.thymeleaf
thymeleaf-spring5
-
- javax.servlet
- javax.servlet-api
+ com.fasterxml.jackson.core
+ jackson-core
-
- org.sakaiproject.onedrive
- sakai-onedrive-api
+ com.fasterxml.jackson.core
+ jackson-annotations
- org.sakaiproject.kernel
- sakai-kernel-api
-
-
- org.sakaiproject.kernel
- sakai-component-manager
+ com.fasterxml.jackson.core
+ jackson-databind
+
- org.sakaiproject.kernel
- sakai-kernel-util
+ javax.servlet
+ javax.servlet-api
+
+
org.apache.commons
commons-lang3
+
+
+
+ org.springframework
+ spring-test
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
src/main/java
-
- ${basedir}/src/main/java
-
- **/*.xml
-
-
${basedir}/src/main/resources
diff --git a/cloud-storage/onedrive/tool/src/main/java/org/sakaiproject/onedrive/ThymeleafConfig.java b/meetings/tool/src/main/java/org/sakaiproject/meetings/ThymeleafConfig.java
similarity index 88%
rename from cloud-storage/onedrive/tool/src/main/java/org/sakaiproject/onedrive/ThymeleafConfig.java
rename to meetings/tool/src/main/java/org/sakaiproject/meetings/ThymeleafConfig.java
index cd9244e1c6a4..253a2fd140b2 100644
--- a/cloud-storage/onedrive/tool/src/main/java/org/sakaiproject/onedrive/ThymeleafConfig.java
+++ b/meetings/tool/src/main/java/org/sakaiproject/meetings/ThymeleafConfig.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2003-2019 The Apereo Foundation
+ * Copyright (c) 2024 The Apereo Foundation
*
* Licensed under the Educational Community License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,7 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.sakaiproject.onedrive;
+package org.sakaiproject.meetings;
+
+import java.nio.charset.StandardCharsets;
import org.sakaiproject.util.ResourceLoaderMessageSource;
import org.springframework.context.ApplicationContext;
@@ -24,8 +26,7 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
-import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
-import org.thymeleaf.TemplateEngine;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.thymeleaf.spring5.ISpringTemplateEngine;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
@@ -35,9 +36,8 @@
@Configuration
@EnableWebMvc
-@ComponentScan("org.sakaiproject.onedrive")
-public class ThymeleafConfig extends WebMvcConfigurerAdapter implements ApplicationContextAware {
- private static final String UTF8 = "UTF-8";
+@ComponentScan("org.sakaiproject.meetings")
+public class ThymeleafConfig implements WebMvcConfigurer, ApplicationContextAware {
private ApplicationContext applicationContext;
@@ -57,7 +57,7 @@ public MessageSource messageSource() {
public ViewResolver viewResolver() {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine());
- viewResolver.setCharacterEncoding(UTF8);
+ viewResolver.setCharacterEncoding(StandardCharsets.UTF_8.name());
return viewResolver;
}
@@ -77,4 +77,5 @@ private ITemplateResolver templateResolver() {
templateResolver.setTemplateMode(TemplateMode.HTML);
return templateResolver;
}
+
}
diff --git a/meetings/tool/src/main/java/org/sakaiproject/meetings/WebAppConfiguration.java b/meetings/tool/src/main/java/org/sakaiproject/meetings/WebAppConfiguration.java
new file mode 100644
index 000000000000..f69e57b7ff1a
--- /dev/null
+++ b/meetings/tool/src/main/java/org/sakaiproject/meetings/WebAppConfiguration.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2024 The Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sakaiproject.meetings;
+
+import java.util.EnumSet;
+
+import javax.servlet.DispatcherType;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRegistration.Dynamic;
+
+import org.sakaiproject.util.RequestFilter;
+import org.sakaiproject.util.SakaiContextLoaderListener;
+import org.sakaiproject.util.ToolListener;
+import org.springframework.web.WebApplicationInitializer;
+import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
+import org.springframework.web.servlet.DispatcherServlet;
+
+public class WebAppConfiguration implements WebApplicationInitializer {
+
+ @Override
+ public void onStartup(ServletContext servletContext) throws ServletException {
+ AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
+ rootContext.setServletContext(servletContext);
+ rootContext.register(ThymeleafConfig.class);
+
+ servletContext.addListener(new ToolListener());
+ servletContext.addListener(new SakaiContextLoaderListener(rootContext));
+
+ servletContext.addFilter("sakai.request", RequestFilter.class)
+ .addMappingForUrlPatterns(
+ EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE),
+ true,
+ "/*");
+
+ Dynamic servlet = servletContext.addServlet("sakai.meetings", new DispatcherServlet(rootContext));
+ servlet.addMapping("/");
+ servlet.setLoadOnStartup(1);
+ }
+
+}
diff --git a/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/GlobalController.java b/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/GlobalController.java
new file mode 100644
index 000000000000..0a5cbc57b333
--- /dev/null
+++ b/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/GlobalController.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2023 Apereo Foundation
+ *
+ * Licensed under the Educational Community License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/ecl2
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sakaiproject.meetings.controller;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.sakaiproject.exception.IdUnusedException;
+import org.sakaiproject.meetings.exceptions.MeetingsException;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+
+
+/**
+ * GlobalController
+ *
+ * This controller is used to handle exceptions
+ *
+ */
+@ControllerAdvice
+public class GlobalController {
+
+ @ExceptionHandler(value = {MeetingsException.class, IdUnusedException.class})
+ public ResponseEntity |