diff --git a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF index e0b4ced494..72a1fe2221 100644 --- a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF @@ -20,6 +20,7 @@ Export-Package: io.cloudbeaver, io.cloudbeaver.auth.provider, io.cloudbeaver.auth.provider.fa, io.cloudbeaver.auth.provider.local, + io.cloudbeaver.events, io.cloudbeaver.model, io.cloudbeaver.model.app, io.cloudbeaver.model.rm, diff --git a/server/bundles/io.cloudbeaver.model/plugin.xml b/server/bundles/io.cloudbeaver.model/plugin.xml index 572b6e6f11..1bbc06fb18 100644 --- a/server/bundles/io.cloudbeaver.model/plugin.xml +++ b/server/bundles/io.cloudbeaver.model/plugin.xml @@ -3,4 +3,5 @@ + diff --git a/server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.event.handler.exsd b/server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.event.handler.exsd new file mode 100644 index 0000000000..93269606b0 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.event.handler.exsd @@ -0,0 +1,11 @@ + + + + + + + + Web events handler + + + diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEvent.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEvent.java new file mode 100644 index 0000000000..635d835d50 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEvent.java @@ -0,0 +1,50 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2022 DBeaver Corp and others + * + * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0 + * + * 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 io.cloudbeaver.events; + +import org.jkiss.code.NotNull; + +import java.util.Map; + +/** + * CloudBeaver event + */ +public class CBEvent { + @NotNull + private final String eventType; + @NotNull + private final Map eventData; + + public CBEvent(@NotNull String eventType) { + this(eventType, Map.of()); + } + + public CBEvent(@NotNull String eventType, @NotNull Map eventData) { + this.eventType = eventType; + this.eventData = eventData; + } + + @NotNull + public String getEventType() { + return eventType; + } + + @NotNull + public Map getEventData() { + return eventData; + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventConstants.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventConstants.java new file mode 100644 index 0000000000..35aabe6f19 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventConstants.java @@ -0,0 +1,24 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2022 DBeaver Corp and others + * + * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0 + * + * 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 io.cloudbeaver.events; + +/** + * CB event constants + */ +public interface CBEventConstants { + String CLOUDBEAVER_CONFIG_CHANGED = "cb_config_changed"; +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventController.java new file mode 100644 index 0000000000..011d83c75f --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventController.java @@ -0,0 +1,86 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2022 DBeaver Corp and others + * + * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0 + * + * 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 io.cloudbeaver.events; + +import io.cloudbeaver.events.registry.CBEventHandlersRegistry; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.runtime.AbstractJob; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CBEventController { + private final Map> eventHandlersByType = new HashMap<>(); + + public CBEventController() { + var eventHandlers = CBEventHandlersRegistry.getInstance().getEventHandlers(); + + eventHandlers + .forEach(handler -> eventHandlersByType.computeIfAbsent(handler.getSupportedEventType(), x -> new ArrayList<>()).add(handler)); + } + + private final List eventsPool = new ArrayList<>(); + + /** + * Add cb event to the event pool + */ + public void addEvent(@NotNull CBEvent event) { + synchronized (eventsPool) { + eventsPool.add(event); + } + } + + /** + * Add cb event to the event pool + */ + public void scheduleCheckJob() { + new CBEventCheckJob().schedule(); + } + + private class CBEventCheckJob extends AbstractJob { + private static final long CHECK_PERIOD = 1000; + + protected CBEventCheckJob() { + super("CloudBeaver events job"); + } + + @Override + protected IStatus run(DBRProgressMonitor monitor) { + List events; + + synchronized (eventsPool) { + events = List.copyOf(eventsPool); + eventsPool.clear(); + } + if (events.isEmpty()) { + schedule(CHECK_PERIOD); + return Status.OK_STATUS; + } + for (CBEvent event : events) { + eventHandlersByType.getOrDefault(event.getEventType(), List.of()) + .forEach(handler -> handler.handleEvent(event)); + } + schedule(CHECK_PERIOD); + return Status.OK_STATUS; + } + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventHandler.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventHandler.java new file mode 100644 index 0000000000..574c0082fa --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventHandler.java @@ -0,0 +1,35 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2022 DBeaver Corp and others + * + * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0 + * + * 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 io.cloudbeaver.events; + +import org.jkiss.code.NotNull; + +/** + * CB web event handlers + */ +public interface CBEventHandler { + /** + * Expected cb event type + */ + @NotNull + String getSupportedEventType(); + + /** + * Handle associated cb event + */ + void handleEvent(@NotNull CBEvent event); +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlerDescriptor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlerDescriptor.java new file mode 100644 index 0000000000..65fad36ee6 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlerDescriptor.java @@ -0,0 +1,44 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2022 DBeaver Corp and others + * + * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0 + * + * 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 io.cloudbeaver.events.registry; + +import io.cloudbeaver.events.CBEventHandler; +import org.eclipse.core.runtime.IConfigurationElement; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.impl.AbstractDescriptor; + +/** + * CB event handler descriptor + */ +public class CBEventHandlerDescriptor extends AbstractDescriptor { + private final ObjectType implType; + + protected CBEventHandlerDescriptor(IConfigurationElement contributorConfig) { + super(contributorConfig); + this.implType = new ObjectType(contributorConfig, "class"); + } + + @NotNull + public CBEventHandler getInstance() { + try { + return implType.createInstance(CBEventHandler.class); + } catch (DBException e) { + throw new IllegalStateException("Can not instantiate event handler '" + implType.getImplName() + "'", e); + } + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlersRegistry.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlersRegistry.java new file mode 100644 index 0000000000..6cd82eb415 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlersRegistry.java @@ -0,0 +1,61 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2022 DBeaver Corp and others + * + * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0 + * + * 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 io.cloudbeaver.events.registry; + +import io.cloudbeaver.events.CBEventHandler; +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.Platform; +import org.jkiss.code.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class CBEventHandlersRegistry { + private static final String EXTENSION_ID = "io.cloudbeaver.event.handler"; + private static final String EVENT_HANDLER_TAG = "eventHandler"; + private static CBEventHandlersRegistry instance = null; + + public synchronized static CBEventHandlersRegistry getInstance() { + if (instance == null) { + instance = new CBEventHandlersRegistry(); + } + return instance; + } + + @NotNull + public List getEventHandlers() { + List eventHandlerDescriptors = readDescriptors(); + + return eventHandlerDescriptors.stream() + .map(CBEventHandlerDescriptor::getInstance) + .collect(Collectors.toList()); + } + + @NotNull + private List readDescriptors() { + var result = new ArrayList(); + var registry = Platform.getExtensionRegistry(); + for (IConfigurationElement ext : registry.getConfigurationElementsFor(EXTENSION_ID)) { + // Load webServices + if (EVENT_HANDLER_TAG.equals(ext.getName())) { + result.add(new CBEventHandlerDescriptor(ext)); + } + } + return result; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java index aec7166ab2..99f2d8d325 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java @@ -20,6 +20,7 @@ import io.cloudbeaver.DBWConstants; import io.cloudbeaver.DBWebException; import io.cloudbeaver.DataSourceFilter; import io.cloudbeaver.VirtualProjectImpl; +import io.cloudbeaver.events.CBEvent; import io.cloudbeaver.model.WebAsyncTaskInfo; import io.cloudbeaver.model.WebConnectionInfo; import io.cloudbeaver.model.WebServerMessage; @@ -124,6 +125,7 @@ public class WebSession extends AbstractSessionPersistent private final WebApplication application; private final Map sessionHandlers; private final WebUserContext userContext; + private final List sessionEvents = new ArrayList<>(); public WebSession(HttpSession httpSession, WebApplication application, @@ -723,6 +725,27 @@ public class WebSession extends AbstractSessionPersistent addSessionMessage(new WebServerMessage(WebServerMessage.MessageType.INFO, message)); } + public void addSessionEvent(CBEvent event) { + synchronized (sessionEvents) { + sessionEvents.add(event); + } + } + + public List getSessionEvents(int eventsCount) { + synchronized (sessionEvents) { + List result; + if (sessionEvents.size() <= eventsCount) { + result = List.copyOf(sessionEvents); + sessionEvents.clear(); + } else { + var subList = sessionEvents.subList(0, eventsCount - 1); + result = List.copyOf(subList); + subList.clear(); + } + return result; + } + } + public List readLog(Integer maxEntries, Boolean clearLog) { synchronized (sessionMessages) { List messages = new ArrayList<>(); diff --git a/server/bundles/io.cloudbeaver.server/plugin.xml b/server/bundles/io.cloudbeaver.server/plugin.xml index 93a4bb342e..48e94d9dbf 100644 --- a/server/bundles/io.cloudbeaver.server/plugin.xml +++ b/server/bundles/io.cloudbeaver.server/plugin.xml @@ -15,6 +15,7 @@ + @@ -22,13 +23,16 @@ - + - + - + @@ -44,4 +48,8 @@ + + + + diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index b753393dfe..d8652e7f17 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -386,6 +386,15 @@ type LogEntry { stackTrace: String } +enum CBEventType { + cb_config_changed +} + +type CBEvent { + eventType: CBEventType! + eventData: Object! +} + #################################################### # Input types #################################################### @@ -503,6 +512,8 @@ extend type Query { listProjects: [ ProjectInfo! ]! readSessionLog(maxEntries: Int, clearEntries: Boolean): [ LogEntry! ]! + + readSessionEvents(maxEntries: Int!): [ CBEvent! ]! } extend type Mutation { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java index be1a1ea151..7d4ff0593d 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java @@ -21,6 +21,9 @@ import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.auth.NoAuthCredentialsProvider; +import io.cloudbeaver.events.CBEvent; +import io.cloudbeaver.events.CBEventConstants; +import io.cloudbeaver.events.CBEventController; import io.cloudbeaver.model.app.BaseWebApplication; import io.cloudbeaver.model.app.WebAuthApplication; import io.cloudbeaver.model.app.WebAuthConfiguration; @@ -121,6 +124,8 @@ public class CBApplication extends BaseWebApplication implements WebAuthApplicat private String localHostAddress; private final List localInetAddresses = new ArrayList<>(); + protected final CBEventController eventController = new CBEventController(); + private WebSessionManager sessionManager; public CBApplication() { @@ -342,6 +347,8 @@ public class CBApplication extends BaseWebApplication implements WebAuthApplicat System.setSecurityManager(new SecurityManager()); } + eventController.scheduleCheckJob(); + runWebServer(); log.debug("Shutdown"); @@ -440,7 +447,7 @@ public class CBApplication extends BaseWebApplication implements WebAuthApplicat @NotNull private File getRuntimeAppConfigFile() { - return getDataDirectory(true).resolve(CBConstants.RUNTIME_APP_CONFIG_FILE_NAME).toFile(); + return getDataDirectory(true).resolve(CBConstants.RUNTIME_APP_CONFIG_FILE_NAME).toFile(); } @NotNull @@ -789,6 +796,7 @@ public class CBApplication extends BaseWebApplication implements WebAuthApplicat } configurationMode = CommonUtils.isEmpty(serverName); + eventController.addEvent(new CBEvent(CBEventConstants.CLOUDBEAVER_CONFIG_CHANGED)); } protected Map readRuntimeConfigurationProperties() throws DBException { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBConfigurationChangedEventHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBConfigurationChangedEventHandler.java new file mode 100644 index 0000000000..9bafc539aa --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBConfigurationChangedEventHandler.java @@ -0,0 +1,51 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2022 DBeaver Corp and others + * + * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0 + * + * 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 io.cloudbeaver.server.events; + +import io.cloudbeaver.events.CBEvent; +import io.cloudbeaver.events.CBEventConstants; +import io.cloudbeaver.events.CBEventHandler; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.server.CBPlatform; +import io.cloudbeaver.service.session.WebSessionManager; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; + +import java.util.Collection; + +/** + * Notify all active user session that configuration has been changed + */ +public class CBConfigurationChangedEventHandler implements CBEventHandler { + private static final Log log = Log.getLog(CBConfigurationChangedEventHandler.class); + + @NotNull + @Override + public String getSupportedEventType() { + return CBEventConstants.CLOUDBEAVER_CONFIG_CHANGED; + } + + @Override + public void handleEvent(@NotNull CBEvent event) { + log.debug(getSupportedEventType() + " event handled"); + Collection allSessions = CBPlatform.getInstance().getSessionManager().getAllActiveSessions(); + + for (WebSession activeUserSession : allSessions) { + activeUserSession.addSessionEvent(event); + } + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java index fc8346c619..666d0f33a9 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java @@ -20,6 +20,7 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebAction; import io.cloudbeaver.WebObjectId; import io.cloudbeaver.WebProjectAction; +import io.cloudbeaver.events.CBEvent; import io.cloudbeaver.model.*; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.DBWService; @@ -84,6 +85,9 @@ public interface DBWServiceCore extends DBWService { @WebAction List readSessionLog(@NotNull WebSession webSession, Integer maxEntries, Boolean clearEntries) throws DBWebException; + @WebAction(requirePermissions = {}) + List readSessionEvents(@Nullable WebSession webSession, Integer maxEntries) throws DBWebException; + @WebAction(requirePermissions = {}) boolean closeSession(HttpServletRequest request) throws DBWebException; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java index 2328f86ac9..7dba9a5676 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java @@ -73,7 +73,9 @@ public class WebServiceBindingCore extends WebServiceBindingBase getWebSession(env), getProjectReference(env), env.getArgument("id"))) .dataFetcher("listProjects", env -> getService(env).getProjects(getWebSession(env))) - + .dataFetcher("readSessionEvents", env -> getService(env).readSessionEvents( + findWebSession(env), env.getArgument("maxEntries") + )) .dataFetcher("readSessionLog", env -> { // CB-90. Log read mustn't extend session lifetime and mustn't fail if there is no session. WebSession session = findWebSession(env); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java index 204cd5b918..7889510c17 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java @@ -20,6 +20,7 @@ package io.cloudbeaver.service.core.impl; import io.cloudbeaver.DBWConstants; import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebServiceUtils; +import io.cloudbeaver.events.CBEvent; import io.cloudbeaver.model.*; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.registry.WebHandlerRegistry; @@ -51,6 +52,7 @@ import org.jkiss.dbeaver.model.net.DBWTunnel; import org.jkiss.dbeaver.model.net.ssh.SSHImplementation; import org.jkiss.dbeaver.model.rm.RMProjectType; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.registry.BaseProjectImpl; import org.jkiss.dbeaver.registry.DataSourceDescriptor; import org.jkiss.dbeaver.registry.DataSourceProviderRegistry; import org.jkiss.dbeaver.registry.network.NetworkHandlerDescriptor; @@ -177,12 +179,25 @@ public class WebServiceCore implements DBWServiceCore { public List getConnectionFolders( @NotNull WebSession webSession, @Nullable String projectId, @Nullable String id ) throws DBWebException { + if (projectId == null) { + return webSession.getAccessibleProjects().stream() + .flatMap(pr -> getConnectionFoldersFromProject(webSession, pr).stream()) + .collect(Collectors.toList()); + } if (id != null) { WebConnectionFolderInfo folderInfo = WebConnectionFolderUtils.getFolderInfo(webSession, projectId, id); return Collections.singletonList(folderInfo); } - return webSession.getProjectById(projectId).getDataSourceRegistry().getAllFolders().stream() - .map(f -> new WebConnectionFolderInfo(webSession, f)).collect(Collectors.toList()); + DBPProject project = webSession.getProjectById(projectId); + return getConnectionFoldersFromProject(webSession, project); + } + + private List getConnectionFoldersFromProject( + @NotNull WebSession webSession, + @NotNull DBPProject project + ) { + return project.getDataSourceRegistry().getAllFolders().stream() + .map(f -> new WebConnectionFolderInfo(webSession, f)).collect(Collectors.toList()); } @Override @@ -225,6 +240,14 @@ public class WebServiceCore implements DBWServiceCore { return webSession.readLog(maxEntries, clearEntries); } + @Override + public List readSessionEvents(@Nullable WebSession webSession, Integer maxEntries) throws DBWebException { + if (webSession == null) { + return Collections.emptyList(); + } + return webSession.getSessionEvents(maxEntries); + } + @Override public boolean closeSession(HttpServletRequest request) throws DBWebException { try { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java index 2d074040dc..cfa5ead58a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java @@ -188,4 +188,9 @@ public class WebSessionManager { } } + public Collection getAllActiveSessions() { + synchronized (sessionMap) { + return sessionMap.values(); + } + } } diff --git a/webapp/packages/core-root/src/ServerConfigEventHandler.ts b/webapp/packages/core-root/src/ServerConfigEventHandler.ts new file mode 100644 index 0000000000..4d76e34da3 --- /dev/null +++ b/webapp/packages/core-root/src/ServerConfigEventHandler.ts @@ -0,0 +1,32 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { injectable } from '@cloudbeaver/core-di'; +import { + CbEvent, + ResourceEventHandler +} from '@cloudbeaver/core-sdk'; + +import { SessionEvent, SessionEventSource, SessionEventType } from './SessionEventSource'; + +@injectable() +export class ServerConfigEventHandler extends ResourceEventHandler { + constructor( + sessionEventSource: SessionEventSource + ) { + super(sessionEventSource); + } + + map(event: CbEvent): null { + return null; + } + + filter(event: CbEvent): boolean { + return event.eventType === SessionEventType.CbConfigChanged; + } +} diff --git a/webapp/packages/core-root/src/ServerConfigResource.ts b/webapp/packages/core-root/src/ServerConfigResource.ts index 3476c6aeb3..4c22f929c2 100644 --- a/webapp/packages/core-root/src/ServerConfigResource.ts +++ b/webapp/packages/core-root/src/ServerConfigResource.ts @@ -13,6 +13,7 @@ import { GraphQLService, CachedDataResource, ServerConfig, ServerConfigInput, Na import { isArraysEqual } from '@cloudbeaver/core-utils'; import { isNavigatorViewSettingsEqual } from './ConnectionNavigatorViewSettings'; +import { ServerConfigEventHandler } from './ServerConfigEventHandler'; @injectable() export class ServerConfigResource extends CachedDataResource { @@ -20,7 +21,8 @@ export class ServerConfigResource extends CachedDataResource undefined); } get redirectOnFederatedAuth(): boolean { diff --git a/webapp/packages/core-root/src/SessionEventSource.ts b/webapp/packages/core-root/src/SessionEventSource.ts new file mode 100644 index 0000000000..327a760520 --- /dev/null +++ b/webapp/packages/core-root/src/SessionEventSource.ts @@ -0,0 +1,36 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { injectable } from '@cloudbeaver/core-di'; +import { + GraphQLService, + EventSource, + CbEvent, + CbEventType as SessionEventType +} from '@cloudbeaver/core-sdk'; + +export type SessionEvent = CbEvent; +export { SessionEventType }; + +@injectable() +export class SessionEventSource extends EventSource { + constructor( + private readonly graphQLService: GraphQLService + ) { + super(5000); + } + protected async listener(): Promise { + const { events } = await this.graphQLService.sdk.getSessionEvents({ + maxEntries: 1000, + }); + + for (const event of events) { + this.event(event); + } + } +} diff --git a/webapp/packages/core-root/src/manifest.ts b/webapp/packages/core-root/src/manifest.ts index 7dfcb262bf..7c01cb70d7 100644 --- a/webapp/packages/core-root/src/manifest.ts +++ b/webapp/packages/core-root/src/manifest.ts @@ -12,11 +12,13 @@ import { FeaturesResource } from './FeaturesResource'; import { NetworkStateService } from './NetworkStateService'; import { PermissionsService } from './PermissionsService'; import { QuotasService } from './QuotasService'; +import { ServerConfigEventHandler } from './ServerConfigEventHandler'; import { ServerConfigResource } from './ServerConfigResource'; import { ServerService } from './ServerService'; import { ServerSettingsService } from './ServerSettingsService'; import { SessionActionService } from './SessionActionService'; import { SessionDataResource } from './SessionDataResource'; +import { SessionEventSource } from './SessionEventSource'; import { SessionExpireService } from './SessionExpireService'; import { SessionPermissionsResource } from './SessionPermissionsResource'; import { SessionResource } from './SessionResource'; @@ -46,5 +48,7 @@ export const manifest: PluginManifest = { SessionSettingsService, WindowEventsService, QuotasService, + ServerConfigEventHandler, + SessionEventSource, ], }; diff --git a/webapp/packages/core-sdk/src/Resource/EventSource.ts b/webapp/packages/core-sdk/src/Resource/EventSource.ts new file mode 100644 index 0000000000..34f68d7cb2 --- /dev/null +++ b/webapp/packages/core-sdk/src/Resource/EventSource.ts @@ -0,0 +1,107 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; + +import type { CachedResource } from './CachedResource'; + +export type EventSourceCallback = (event: T) => any; +export interface IEventSource { + on( + resource: EventSourceCallback, + mapTo: (param: TEvent) => T, + filter?: (param: TEvent) => boolean, + ): void; +} + +const DEFAULT_INTERVAL = 1000; + +export abstract class EventSource implements IEventSource { + onEvent: ISyncExecutor; + + private listening: boolean; + private readonly interval: number; + constructor(interval?: number) { + this.onEvent = new SyncExecutor(); + this.interval = interval ?? DEFAULT_INTERVAL; + this.listening = false; + } + + on( + resource: EventSourceCallback, + mapTo: (param: TEvent) => T, + filter?: (param: TEvent) => boolean, + ): void; + on( + resource: CachedResource, + mapTo: (param: TEvent) => T, + filter?: (param: TEvent) => boolean, + ): void; + on( + resource: EventSourceCallback | CachedResource, + mapTo: (param: TEvent) => T, + filter?: (param: TEvent) => boolean, + ): void { + if (typeof resource === 'function') { + this.onEvent.addHandler(event => { + if (!filter || filter(event)) { + resource(mapTo(event)); + } + }); + } else { + this.onEvent.addHandler(event => { + if (!filter || filter(event)) { + resource.markOutdated(mapTo(event)); + } + }); + } + + if (!this.listening) { + this.listen(); + } + } + + listen() { + if (this.listening) { + console.warn('Already listening events'); + return; + } + + this.listening = true; + let failedRequests = 0; + let interval = this.interval; + + const iteration = async () => { + try { + await this.listener(); + failedRequests = 0; + } catch (exception: any) { + console.error(exception); + failedRequests++; + } + + interval = this.interval * Math.min((failedRequests || 1), 10); + + if (this.listening) { + setTimeout(iteration, interval); + } + }; + + iteration(); + } + + stop() { + this.listening = false; + } + + event(event: TEvent) { + this.onEvent.execute(event); + } + + protected abstract listener(): Promise; +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/Resource/ResourceEventHandler.ts b/webapp/packages/core-sdk/src/Resource/ResourceEventHandler.ts new file mode 100644 index 0000000000..04d0e6962a --- /dev/null +++ b/webapp/packages/core-sdk/src/Resource/ResourceEventHandler.ts @@ -0,0 +1,64 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; + +import type { CachedResource } from './CachedResource'; +import type { IEventSource, EventSourceCallback } from './EventSource'; + +export abstract class ResourceEventHandler implements IEventSource { + onEvent: ISyncExecutor; + + private subscribed: boolean; + constructor(private readonly source?: IEventSource) { + this.onEvent = new SyncExecutor(); + this.subscribed = false; + } + + on( + resource: EventSourceCallback, + mapTo: (param: TEvent) => T, + filter?: (param: TEvent) => boolean, + ): void; + on( + resource: CachedResource, + mapTo: (param: TEvent) => T, + filter?: (param: TEvent) => boolean, + ): void; + on( + resource: CachedResource | EventSourceCallback, + mapTo: (param: TEvent) => T, + filter?: (param: TEvent) => boolean, + ): void { + if (typeof resource === 'function') { + this.onEvent.addHandler(event => { + if (!filter || filter(event)) { + resource(mapTo(event)); + } + }); + } else { + this.onEvent.addHandler(event => { + if (!filter || filter(event)) { + resource.markOutdated(mapTo(event)); + } + }); + } + + if (!this.subscribed) { + this.subscribed = true; + this.source?.on(this.event.bind(this), this.map.bind(this), this.filter.bind(this)); + } + } + + event(event: TEvent) { + this.onEvent.execute(event); + } + + abstract map(event: SourceEvent): TEvent; + abstract filter(event: SourceEvent): boolean; +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/index.ts b/webapp/packages/core-sdk/src/index.ts index 6326f2d06e..bfbb07a705 100644 --- a/webapp/packages/core-sdk/src/index.ts +++ b/webapp/packages/core-sdk/src/index.ts @@ -5,6 +5,8 @@ export * from './Resource/CachedMapResource'; export * from './Resource/CachedResource'; export * from './Resource/CachedResourceIncludes'; export * from './Resource/ResourceKeyList'; +export * from './Resource/EventSource'; +export * from './Resource/ResourceEventHandler'; export * from './CustomGraphQLClient'; export * from './DetailsError'; export * from './EnvironmentService'; diff --git a/webapp/packages/core-sdk/src/queries/session/getSessionEvents.gql b/webapp/packages/core-sdk/src/queries/session/getSessionEvents.gql new file mode 100644 index 0000000000..d4a04d5844 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/session/getSessionEvents.gql @@ -0,0 +1,10 @@ +query getSessionEvents( + $maxEntries: Int! +) { + events: readSessionEvents( + maxEntries: $maxEntries + ) { + eventType + eventData + } +} diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index f3967c8b19..22429b1d07 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -163,6 +163,15 @@ export enum AuthStatus { Success = 'SUCCESS' } +export interface CbEvent { + eventData: Scalars['Object']; + eventType: CbEventType; +} + +export enum CbEventType { + CbConfigChanged = 'cb_config_changed' +} + export interface ConnectionConfig { authModelId?: InputMaybe; configurationType?: InputMaybe; @@ -909,6 +918,7 @@ export interface Query { navNodeParents: Array; navRefreshNode?: Maybe; networkHandlers: Array; + readSessionEvents: Array; readSessionLog: Array; revokeUserTeam?: Maybe; rmListProjectGrantedPermissions: Array; @@ -1151,6 +1161,11 @@ export interface QueryNavRefreshNodeArgs { } +export interface QueryReadSessionEventsArgs { + maxEntries: Scalars['Int']; +} + + export interface QueryReadSessionLogArgs { clearEntries?: InputMaybe; maxEntries?: InputMaybe; @@ -2639,6 +2654,13 @@ export type ChangeSessionLanguageMutationVariables = Exact<{ export type ChangeSessionLanguageMutation = { changeSessionLanguage?: boolean }; +export type GetSessionEventsQueryVariables = Exact<{ + maxEntries: Scalars['Int']; +}>; + + +export type GetSessionEventsQuery = { events: Array<{ eventType: CbEventType, eventData: any }> }; + export type OpenSessionMutationVariables = Exact<{ defaultLocale?: InputMaybe; }>; @@ -4215,6 +4237,14 @@ export const ChangeSessionLanguageDocument = ` changeSessionLanguage(locale: $locale) } `; +export const GetSessionEventsDocument = ` + query getSessionEvents($maxEntries: Int!) { + events: readSessionEvents(maxEntries: $maxEntries) { + eventType + eventData + } +} + `; export const OpenSessionDocument = ` mutation openSession($defaultLocale: String) { session: openSession(defaultLocale: $defaultLocale) { @@ -4735,6 +4765,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = changeSessionLanguage(variables: ChangeSessionLanguageMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(ChangeSessionLanguageDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'changeSessionLanguage', 'mutation'); }, + getSessionEvents(variables: GetSessionEventsQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(GetSessionEventsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getSessionEvents', 'query'); + }, openSession(variables?: OpenSessionMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(OpenSessionDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'openSession', 'mutation'); },