From 659084b14126a16fb75ab8d4236c21da2a105b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Dywicki?= Date: Fri, 10 May 2024 01:49:05 +0200 Subject: [PATCH] Simplify chained profile implementation. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #85. Earlier implementation from 2021/2022 did rely on thread local. While collision at thread local were unlikely to spot, overall architecture of this component become fairly complex. In order to simplify and keep it in clean shape internal call flow have been redesigned to rely on local stack. Since all profiles are known before hand, we can use order of their creation as well as intention (callback method call), to determine direction in which we should navigate further. Once all callbacks are passed, a framework callback is called. Signed-off-by: Ɓukasz Dywicki --- .../internal/state/DummyStateRetriever.java | 2 +- .../internal/CollectorProfileTest.java | 2 +- .../internal/EnergyCounterProfileTest.java | 2 +- .../internal/ChainedProfileCallback.java | 78 ----------- .../profile/internal/ConnectorioProfile.java | 49 +++---- .../profile/internal/NavigableCallback.java | 65 +++++++++ .../internal/StackedProfileCallback.java | 67 ++++++---- .../internal/ConnectorioProfileTest.java | 27 ++-- .../internal/StartLevelDebugCommand.java | 125 ++++++++++++++++++ 9 files changed, 266 insertions(+), 151 deletions(-) delete mode 100644 bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/ChainedProfileCallback.java create mode 100644 bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/NavigableCallback.java create mode 100755 bundles/org.connectorio.addons.startlevel.shell/src/main/java/org/connectorio/addons/startlevel/shell/internal/StartLevelDebugCommand.java diff --git a/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/state/DummyStateRetriever.java b/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/state/DummyStateRetriever.java index e14c5aa4..134af61a 100644 --- a/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/state/DummyStateRetriever.java +++ b/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/state/DummyStateRetriever.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2021 ConnectorIO Sp. z o.o. + * Copyright (C) 2024-2024 ConnectorIO Sp. z o.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/CollectorProfileTest.java b/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/CollectorProfileTest.java index af91620f..df8f4da7 100644 --- a/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/CollectorProfileTest.java +++ b/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/CollectorProfileTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2022 ConnectorIO Sp. z o.o. + * Copyright (C) 2024-2024 ConnectorIO Sp. z o.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/EnergyCounterProfileTest.java b/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/EnergyCounterProfileTest.java index f645c4b5..fb097cb7 100644 --- a/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/EnergyCounterProfileTest.java +++ b/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/EnergyCounterProfileTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2022 ConnectorIO Sp. z o.o. + * Copyright (C) 2024-2024 ConnectorIO Sp. z o.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/ChainedProfileCallback.java b/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/ChainedProfileCallback.java deleted file mode 100644 index 5a887d05..00000000 --- a/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/ChainedProfileCallback.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2019-2021 ConnectorIO Sp. z o.o. - * - * 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. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.connectorio.addons.profile.internal; - -import java.util.Iterator; -import org.openhab.core.thing.profiles.ProfileCallback; -import org.openhab.core.thing.profiles.StateProfile; -import org.openhab.core.types.Command; -import org.openhab.core.types.State; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ChainedProfileCallback implements ProfileCallback { - - private final Logger logger = LoggerFactory.getLogger(ChainedProfileCallback.class); - private final Iterator profiles; - private final ProfileCallback delegate; - - public ChainedProfileCallback(Iterator profiles, ProfileCallback delegate) { - this.profiles = profiles; - this.delegate = delegate; - } - - @Override - public void handleCommand(Command command) { - if (profiles.hasNext()) { - StateProfile next = profiles.next(); - logger.trace("Passing command {} to next profile {}", command, next); - next.onCommandFromItem(command); - } else { - logger.trace("Passing command {} final callback", command); - delegate.handleCommand(command); - } - } - - @Override - public void sendCommand(Command command) { - if (profiles.hasNext()) { - StateProfile next = profiles.next(); - logger.trace("Sending command {} to next profile {}", command, next); - next.onCommandFromHandler(command); - } else { - logger.trace("Sending command {} to final callback", command); - delegate.sendCommand(command); - } - } - - @Override - public void sendUpdate(State state) { - if (profiles.hasNext()) { - StateProfile next = profiles.next(); - logger.trace("Sending state {} to next profile {}", state, next); - next.onStateUpdateFromHandler(state); - } else { - logger.trace("Sending state {} to final callback", state); - delegate.sendUpdate(state); - } - } - - public String toString() { - return "ChainedProfileCallback [" + profiles + ", " + delegate + "]"; - } -} diff --git a/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/ConnectorioProfile.java b/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/ConnectorioProfile.java index 06a69e9d..5b4e3e74 100644 --- a/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/ConnectorioProfile.java +++ b/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/ConnectorioProfile.java @@ -25,8 +25,7 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.Executor; import java.util.function.Consumer; import org.connectorio.addons.profile.ProfileFactoryRegistry; import org.connectorio.addons.profile.internal.util.NestedMapCreator; @@ -48,15 +47,19 @@ class ConnectorioProfile implements StateProfile { private final Logger logger = LoggerFactory.getLogger(ConnectorioProfile.class); - private final ProfileCallback callback; + private final StackedProfileCallback callback; + private final Executor executor; private final ProfileContext context; - private final LinkedList profileChain = new LinkedList<>(); + private final LinkedList callbackChain = new LinkedList<>(); private final ProfileFactoryRegistry registry; - private final ExecutorService executor = Executors.newSingleThreadExecutor(); ConnectorioProfile(ProfileCallback callback, ProfileContext context, ProfileFactoryRegistry registry) { - this.callback = callback; + this(context.getExecutorService(), callback, context, registry); + } + + ConnectorioProfile(Executor executor, ProfileCallback callback, ProfileContext context, ProfileFactoryRegistry registry) { + this.executor = executor; this.context = context; this.registry = registry; @@ -67,7 +70,8 @@ class ConnectorioProfile implements StateProfile { } ItemChannelLink link = determnineLink(callback); - StackedProfileCallback chainedCallback = new StackedProfileCallback(link); + + this.callback = new StackedProfileCallback(callback, callbackChain); for (Entry entry : config.entrySet()) { if ("profile".equals(entry.getKey())) { continue; @@ -79,7 +83,7 @@ class ConnectorioProfile implements StateProfile { Map profileCfg = (Map) entry.getValue(); String profileType = (String) profileCfg.get("profile"); logger.debug("Creating profile {} for config key {}", profileType, entry.getKey()); - Profile createdProfile = getProfileFromFactories(getConfiguredProfileTypeUID(profileType), profileCfg, chainedCallback); + Profile createdProfile = getProfileFromFactories(getConfiguredProfileTypeUID(profileType), profileCfg, new NavigableCallback(link, callbackChain.size(), this.callback)); if (createdProfile == null) { Optional supported = registry.getAll().stream() .map(ProfileFactory::getSupportedProfileTypeUIDs) @@ -91,7 +95,7 @@ class ConnectorioProfile implements StateProfile { if (!(createdProfile instanceof StateProfile)) { throw new IllegalArgumentException("Could not create profile " + profileType + " or it is not state profile"); } - profileChain.add((StateProfile) createdProfile); + callbackChain.add((StateProfile) createdProfile); } } @@ -155,36 +159,15 @@ public void onStateUpdateFromHandler(State state) { } private void handleReading(boolean incoming, Type type, Consumer head) { - context.getExecutorService().execute(new Runnable() { + executor.execute(new Runnable() { @Override public void run() { try { - Iterator delegate = incoming ? profileChain.iterator() : profileChain.descendingIterator(); - Iterator iterator = new Iterator() { - int pos = 0; - @Override - public boolean hasNext() { - return delegate.hasNext(); - } - - @Override - public StateProfile next() { - pos++; - return delegate.next(); - } - - public String toString() { - return "Iterator [" + pos + ", " + profileChain + "]"; - } - }; - ChainedProfileCallback callback = new ChainedProfileCallback(iterator, ConnectorioProfile.this.callback); - logger.trace("Setting chained callback for {} to {}", type, callback); - StackedProfileCallback.set(callback); + Iterator iterator = incoming ? callbackChain.iterator() : callbackChain.descendingIterator(); + logger.trace("Firing chained profiles for {} to {}", type, callback); head.accept(iterator.next()); } catch (Throwable e) { logger.warn("Uncaught error found while calling profile chain for {}", type, e); - } finally { - StackedProfileCallback.set(null); } } }); diff --git a/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/NavigableCallback.java b/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/NavigableCallback.java new file mode 100644 index 00000000..9159e1e4 --- /dev/null +++ b/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/NavigableCallback.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024-2024 ConnectorIO Sp. z o.o. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.connectorio.addons.profile.internal; + +import org.openhab.core.thing.link.ItemChannelLink; +import org.openhab.core.thing.profiles.ProfileCallback; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; + +/** + * Callback implementation which is aware of its position in chain. + * + * When this callback is asked to dispatch command or sate it passes it to chain with upper (state) + * or lower (command) element index. This construction allows to move state/command information + * across entire chain without too complex logic. Finalization of the call happens n stacked profile + * callback which know chain boundaries. + */ +public class NavigableCallback implements ProfileCallback { + + private final ItemChannelLink link; + private final int index; + private final StackedProfileCallback stack; + + public NavigableCallback(ItemChannelLink link, int index, StackedProfileCallback stack) { + this.link = link; + this.index = index; + this.stack = stack; + } + + @Override + public void handleCommand(Command command) { + stack.handleCommand(index - 1, command); + } + + @Override + public void sendCommand(Command command) { + stack.sendCommand(index - 1, command); + } + + @Override + public void sendUpdate(State state) { + stack.sendUpdate(index + 1, state); + } + + @Override + public String toString() { + return "Chained Callback [" + link + " at index " + index + "]"; + } + +} diff --git a/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/StackedProfileCallback.java b/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/StackedProfileCallback.java index d85281e6..1548cd83 100644 --- a/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/StackedProfileCallback.java +++ b/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/StackedProfileCallback.java @@ -17,54 +17,63 @@ */ package org.connectorio.addons.profile.internal; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Executor; import org.openhab.core.thing.link.ItemChannelLink; import org.openhab.core.thing.profiles.ProfileCallback; +import org.openhab.core.thing.profiles.StateProfile; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class StackedProfileCallback implements ProfileCallback { - - private final static ThreadLocal DELEGATE = new ThreadLocal<>(); +/** + * Stacked callback is a reference point for created profiles to communicate with framework. + * + * Since profiles can call callback at any time, this instance must be present when profile is being created. + * This leads to situation that we have to bridge it into future. + * More over, because this callback can be called from handler to item and from item to handler + * it has to work in both directions, independently of the creation time. + */ +public class StackedProfileCallback { private final Logger logger = LoggerFactory.getLogger(StackedProfileCallback.class); - private final ItemChannelLink link; - public StackedProfileCallback(ItemChannelLink link) { - this.link = link; + private final ProfileCallback callback; + private final LinkedList chain; + + public StackedProfileCallback(ProfileCallback callback, LinkedList chain) { + this.callback = callback; + this.chain = chain; } - @Override - public void handleCommand(Command command) { + public void handleCommand(int index, Command command) { + if (index == -1) { + callback.handleCommand(command); + return; + } logger.trace("Passing command {} to profile chain", command); - getDelegate().handleCommand(command); + chain.get(index).onCommandFromItem(command); } - @Override - public void sendCommand(Command command) { + public void sendCommand(int index, Command command) { + if (index == -1) { + callback.handleCommand(command); + return; + } logger.trace("Sending command {} toi profile chain", command); - getDelegate().sendCommand(command); + chain.get(index).onCommandFromHandler(command); } - @Override - public void sendUpdate(State state) { - logger.trace("Sending state {} to profile chain", state); - getDelegate().sendUpdate(state); - } - - private ProfileCallback getDelegate() { - ProfileCallback callback = DELEGATE.get(); - logger.trace("Callback looked up on thread stack {}", callback); - if (callback != null) { - return callback; + public void sendUpdate(int index, State state) { + if (index >= chain.size()) { + callback.sendUpdate(state); + return; } - - throw new IllegalStateException("No callback found on thread stack"); - } - - static void set(ProfileCallback callback) { - DELEGATE.set(callback); + logger.trace("Sending state {} to profile chain", state); + chain.get(index).onStateUpdateFromHandler(state); } } diff --git a/bundles/org.connectorio.addons.profile/src/test/java/org/connectorio/addons/profile/internal/ConnectorioProfileTest.java b/bundles/org.connectorio.addons.profile/src/test/java/org/connectorio/addons/profile/internal/ConnectorioProfileTest.java index 367334d6..f3609e29 100644 --- a/bundles/org.connectorio.addons.profile/src/test/java/org/connectorio/addons/profile/internal/ConnectorioProfileTest.java +++ b/bundles/org.connectorio.addons.profile/src/test/java/org/connectorio/addons/profile/internal/ConnectorioProfileTest.java @@ -24,6 +24,11 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ThreadFactory; import org.connectorio.addons.profile.ProfileFactoryRegistry; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -44,7 +49,6 @@ /** * Test of chained invocations between profiles and framework. */ -@Disabled @ExtendWith(MockitoExtension.class) class ConnectorioProfileTest { @@ -65,11 +69,18 @@ class ConnectorioProfileTest { ProfileFactory factory2; @Test - void checkChainedValue() { + void checkChainedValue() throws Exception { HashMap cfgMap = new HashMap<>(); - cfgMap.put("profiles", Arrays.asList(CONDITION_PROFILE, SCALE_PROFILE)); + cfgMap.put("a1.profile", CONDITION_PROFILE); + cfgMap.put("b2.profile", SCALE_PROFILE); Configuration config = new Configuration(cfgMap); + Executor executor = new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }; when(context.getConfiguration()).thenReturn(config); when(registry.getAll()).thenReturn(Arrays.asList(factory1, factory2)); @@ -85,11 +96,11 @@ void checkChainedValue() { inv.getArgument(2, ProfileContext.class) )); - ConnectorioProfile profile = new ConnectorioProfile(callback, context, registry); - profile.onCommandFromItem(new DecimalType(22.0)); - Mockito.verify(callback).handleCommand(new DecimalType(11.0)); + ConnectorioProfile profile = new ConnectorioProfile(executor, callback, context, registry); +// profile.onCommandFromItem(new DecimalType(22.0)); +// Mockito.verify(callback).handleCommand(new DecimalType(11.0)); - profile.onCommandFromHandler(new DecimalType(11.0)); + profile.onStateUpdateFromHandler(new DecimalType(11.0)); Mockito.verify(callback).sendUpdate(new DecimalType(22.0)); } @@ -144,7 +155,7 @@ public void onStateUpdateFromHandler(State state) { public void onCommandFromItem(Command command) { if (command instanceof DecimalType) { DecimalType dec = (DecimalType) command; - callback.sendUpdate(new DecimalType(dec.doubleValue() / 2)); + callback.handleCommand(new DecimalType(dec.doubleValue() / 2)); } } } diff --git a/bundles/org.connectorio.addons.startlevel.shell/src/main/java/org/connectorio/addons/startlevel/shell/internal/StartLevelDebugCommand.java b/bundles/org.connectorio.addons.startlevel.shell/src/main/java/org/connectorio/addons/startlevel/shell/internal/StartLevelDebugCommand.java new file mode 100755 index 00000000..ca157d03 --- /dev/null +++ b/bundles/org.connectorio.addons.startlevel.shell/src/main/java/org/connectorio/addons/startlevel/shell/internal/StartLevelDebugCommand.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2019-2024 ConnectorIO Sp. z o.o. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.connectorio.addons.startlevel.shell.internal; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.openhab.core.io.console.Console; +import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension; +import org.openhab.core.io.console.extensions.ConsoleCommandExtension; +import org.openhab.core.service.ReadyMarker; +import org.openhab.core.service.ReadyMarkerFilter; +import org.openhab.core.service.ReadyService; +import org.openhab.core.service.ReadyService.ReadyTracker; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * Simple console addon which prints resolved dependencies for ready markers. + */ +@Component(immediate = true, service = ConsoleCommandExtension.class) +public class StartLevelDebugCommand extends AbstractConsoleCommandExtension { + + private final ReadyService readyService; + + @Activate + public StartLevelDebugCommand(@Reference ReadyService readyService) { + super("co7io-start-level-debug", "Debug start level and ready markers"); + this.readyService = readyService; + } + + @Override + public void execute(String[] args, Console console) { + Set ready = new LinkedHashSet<>(); + ReadyTracker readyTracker = new ReadyTracker() { + @Override + public void onReadyMarkerAdded(ReadyMarker readyMarker) { + ready.add(readyMarker); + } + + @Override + public void onReadyMarkerRemoved(ReadyMarker readyMarker) {} + }; + try { + readyService.registerTracker(readyTracker); + } finally { + readyService.unregisterTracker(readyTracker); + } + + Map trackers = trackers(console); + for (ReadyTracker tracker : trackers.keySet()) { + ReadyMarkerFilter filter = trackers.get(tracker); + Set resolved = ready.stream().filter(filter::apply).collect(Collectors.toSet()); + console.println("Tracker " + tracker); + console.println(" Filter " + inspect(filter)); + if (!resolved.isEmpty()) { + console.println(" Resolved:"); + for (ReadyMarker marker : resolved) { + console.println(" - " + marker); + } + } + } + } + + private String inspect(ReadyMarkerFilter filter) { + Class filterClass = filter.getClass(); + try { + Field type = filterClass.getDeclaredField("type"); + if (!type.isAccessible()) { + type.setAccessible(true); + } + Field identifier = filterClass.getDeclaredField("identifier"); + if (!identifier.isAccessible()) { + identifier.setAccessible(true); + } + return type.get(filter) + "=" + identifier.get(filter); + } catch (Exception e) { + return filter.toString(); + } + } + + private Map trackers(Console console) { + try { + Field trackers = readyService.getClass().getDeclaredField("trackers"); + if (!trackers.isAccessible()) { + trackers.setAccessible(true); + } + Object value = trackers.get(readyService); + if (value instanceof Map) { + return (Map) value; + } + + console.println("Unsupported tracker content " + value); + return Collections.emptyMap(); + } catch (Exception e) { + return Collections.emptyMap(); + } + } + + @Override + public List getUsages() { + return Arrays.asList("co7io-start-level-debug"); + } +}