2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.persistence.jdbc.internal.console;
15 import java.util.Arrays;
16 import java.util.Collection;
17 import java.util.Comparator;
18 import java.util.List;
20 import java.util.Map.Entry;
21 import java.util.stream.Collectors;
22 import java.util.stream.Stream;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.core.io.console.Console;
27 import org.openhab.core.io.console.ConsoleCommandCompleter;
28 import org.openhab.core.io.console.StringsCompleter;
29 import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
30 import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
31 import org.openhab.core.persistence.PersistenceService;
32 import org.openhab.core.persistence.PersistenceServiceRegistry;
33 import org.openhab.persistence.jdbc.internal.ItemTableCheckEntry;
34 import org.openhab.persistence.jdbc.internal.ItemTableCheckEntryStatus;
35 import org.openhab.persistence.jdbc.internal.JdbcPersistenceService;
36 import org.openhab.persistence.jdbc.internal.JdbcPersistenceServiceConstants;
37 import org.openhab.persistence.jdbc.internal.exceptions.JdbcSQLException;
38 import org.osgi.service.component.annotations.Activate;
39 import org.osgi.service.component.annotations.Component;
40 import org.osgi.service.component.annotations.Reference;
43 * The {@link JdbcCommandExtension} is responsible for handling console commands
45 * @author Jacob Laursen - Initial contribution
48 @Component(service = ConsoleCommandExtension.class)
49 public class JdbcCommandExtension extends AbstractConsoleCommandExtension implements ConsoleCommandCompleter {
51 private static final String CMD_SCHEMA = "schema";
52 private static final String CMD_TABLES = "tables";
53 private static final String CMD_RELOAD = "reload";
54 private static final String SUBCMD_SCHEMA_CHECK = "check";
55 private static final String SUBCMD_SCHEMA_FIX = "fix";
56 private static final String SUBCMD_TABLES_LIST = "list";
57 private static final String SUBCMD_TABLES_CLEAN = "clean";
58 private static final String PARAMETER_ALL = "all";
59 private static final String PARAMETER_FORCE = "force";
60 private static final StringsCompleter CMD_COMPLETER = new StringsCompleter(
61 List.of(CMD_SCHEMA, CMD_TABLES, CMD_RELOAD), false);
62 private static final StringsCompleter SUBCMD_SCHEMA_COMPLETER = new StringsCompleter(
63 List.of(SUBCMD_SCHEMA_CHECK, SUBCMD_SCHEMA_FIX), false);
64 private static final StringsCompleter SUBCMD_TABLES_COMPLETER = new StringsCompleter(
65 List.of(SUBCMD_TABLES_LIST, SUBCMD_TABLES_CLEAN), false);
67 private final PersistenceServiceRegistry persistenceServiceRegistry;
70 public JdbcCommandExtension(final @Reference PersistenceServiceRegistry persistenceServiceRegistry) {
71 super(JdbcPersistenceServiceConstants.SERVICE_ID, "Interact with the JDBC persistence service.");
72 this.persistenceServiceRegistry = persistenceServiceRegistry;
76 public void execute(String[] args, Console console) {
77 if (args.length < 1 || args.length > 4) {
81 JdbcPersistenceService persistenceService = getPersistenceService();
82 if (persistenceService == null) {
86 if (!execute(persistenceService, args, console)) {
90 } catch (JdbcSQLException e) {
91 console.println(e.toString());
95 private @Nullable JdbcPersistenceService getPersistenceService() {
96 for (PersistenceService persistenceService : persistenceServiceRegistry.getAll()) {
97 if (persistenceService instanceof JdbcPersistenceService) {
98 return (JdbcPersistenceService) persistenceService;
104 private boolean execute(JdbcPersistenceService persistenceService, String[] args, Console console)
105 throws JdbcSQLException {
106 if (args.length > 1 && CMD_TABLES.equalsIgnoreCase(args[0])) {
107 if (SUBCMD_TABLES_LIST.equalsIgnoreCase(args[1])) {
108 listTables(persistenceService, console, args.length == 3 && PARAMETER_ALL.equalsIgnoreCase(args[2]));
110 } else if (SUBCMD_TABLES_CLEAN.equalsIgnoreCase(args[1])) {
111 if (args.length == 3) {
112 cleanupItem(persistenceService, console, args[2], false);
114 } else if (args.length == 4 && PARAMETER_FORCE.equalsIgnoreCase(args[3])) {
115 cleanupItem(persistenceService, console, args[2], true);
118 cleanupTables(persistenceService, console);
122 } else if (args.length > 1 && CMD_SCHEMA.equalsIgnoreCase(args[0])) {
123 if (args.length == 2 && SUBCMD_SCHEMA_CHECK.equalsIgnoreCase(args[1])) {
124 checkSchema(persistenceService, console);
126 } else if (SUBCMD_SCHEMA_FIX.equalsIgnoreCase(args[1])) {
127 if (args.length == 2) {
128 fixSchema(persistenceService, console);
130 } else if (args.length == 3) {
131 fixSchema(persistenceService, console, args[2]);
135 } else if (args.length == 1 && CMD_RELOAD.equalsIgnoreCase(args[0])) {
136 reload(persistenceService, console);
142 private void checkSchema(JdbcPersistenceService persistenceService, Console console) throws JdbcSQLException {
143 List<Entry<String, String>> itemNameToTableName = persistenceService.getItemNameToTableNameMap().entrySet()
144 .stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList());
145 int itemNameMaxLength = Math
146 .max(itemNameToTableName.stream().map(i -> i.getKey().length()).max(Integer::compare).orElse(0), 4);
147 int tableNameMaxLength = Math
148 .max(itemNameToTableName.stream().map(i -> i.getValue().length()).max(Integer::compare).orElse(0), 5);
149 console.println(String.format("%1$-" + (tableNameMaxLength + 2) + "s%2$-" + (itemNameMaxLength + 2) + "s%3$s",
150 "Table", "Item", "Issue"));
151 console.println("-".repeat(tableNameMaxLength) + " " + "-".repeat(itemNameMaxLength) + " " + "-".repeat(64));
152 for (Entry<String, String> entry : itemNameToTableName) {
153 String itemName = entry.getKey();
154 String tableName = entry.getValue();
155 Collection<String> issues = persistenceService.getSchemaIssues(tableName, itemName);
156 if (!issues.isEmpty()) {
157 for (String issue : issues) {
158 console.println(String.format(
159 "%1$-" + (tableNameMaxLength + 2) + "s%2$-" + (itemNameMaxLength + 2) + "s%3$s", tableName,
166 private void fixSchema(JdbcPersistenceService persistenceService, Console console) {
167 List<Entry<String, String>> itemNameToTableName = persistenceService.getItemNameToTableNameMap().entrySet()
168 .stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList());
169 for (Entry<String, String> entry : itemNameToTableName) {
170 String itemName = entry.getKey();
171 String tableName = entry.getValue();
172 fixSchema(persistenceService, console, tableName, itemName);
176 private void fixSchema(JdbcPersistenceService persistenceService, Console console, String itemName) {
177 Map<String, String> itemNameToTableNameMap = persistenceService.getItemNameToTableNameMap();
178 String tableName = itemNameToTableNameMap.get(itemName);
179 if (tableName != null) {
180 fixSchema(persistenceService, console, tableName, itemName);
182 console.println("Table not found for item '" + itemName + "'");
186 private void fixSchema(JdbcPersistenceService persistenceService, Console console, String tableName,
189 if (persistenceService.fixSchemaIssues(tableName, itemName)) {
190 console.println("Fixed table '" + tableName + "' for item '" + itemName + "'");
192 } catch (JdbcSQLException e) {
193 console.println("Failed to fix table '" + tableName + "' for item '" + itemName + "': " + e.getMessage());
197 private void listTables(JdbcPersistenceService persistenceService, Console console, boolean all)
198 throws JdbcSQLException {
199 List<ItemTableCheckEntry> entries = persistenceService.getCheckedEntries();
201 entries.removeIf(t -> t.getStatus() == ItemTableCheckEntryStatus.VALID);
203 entries.sort(Comparator.comparing(ItemTableCheckEntry::getTableName));
204 int itemNameMaxLength = Math
205 .max(entries.stream().map(t -> t.getItemName().length()).max(Integer::compare).orElse(0), 4);
206 int tableNameMaxLength = Math
207 .max(entries.stream().map(t -> t.getTableName().length()).max(Integer::compare).orElse(0), 5);
208 int statusMaxLength = Stream.of(ItemTableCheckEntryStatus.values()).map(t -> t.toString().length())
209 .max(Integer::compare).get();
210 console.println(String.format(
211 "%1$-" + (tableNameMaxLength + 2) + "sRow Count %2$-" + (itemNameMaxLength + 2) + "s%3$s", "Table",
213 console.println("-".repeat(tableNameMaxLength) + " " + "--------- " + "-".repeat(itemNameMaxLength) + " "
214 + "-".repeat(statusMaxLength));
215 for (ItemTableCheckEntry entry : entries) {
216 String tableName = entry.getTableName();
217 ItemTableCheckEntryStatus status = entry.getStatus();
218 long rowCount = status == ItemTableCheckEntryStatus.VALID
219 || status == ItemTableCheckEntryStatus.ITEM_MISSING ? persistenceService.getRowCount(tableName) : 0;
220 console.println(String.format(
221 "%1$-" + (tableNameMaxLength + 2) + "s%2$9d %3$-" + (itemNameMaxLength + 2) + "s%4$s", tableName,
222 rowCount, entry.getItemName(), status));
226 private void cleanupTables(JdbcPersistenceService persistenceService, Console console) throws JdbcSQLException {
227 console.println("Cleaning up all inconsistent items...");
228 List<ItemTableCheckEntry> entries = persistenceService.getCheckedEntries();
229 entries.removeIf(t -> t.getStatus() == ItemTableCheckEntryStatus.VALID || t.getItemName().isEmpty());
230 for (ItemTableCheckEntry entry : entries) {
231 console.print(entry.getItemName() + " -> ");
232 if (persistenceService.cleanupItem(entry)) {
233 console.println("done.");
235 console.println("skipped/failed.");
240 private void cleanupItem(JdbcPersistenceService persistenceService, Console console, String itemName, boolean force)
241 throws JdbcSQLException {
242 console.print("Cleaning up item " + itemName + "... ");
243 if (persistenceService.cleanupItem(itemName, force)) {
244 console.println("done.");
246 console.println("skipped/failed.");
250 private void reload(JdbcPersistenceService persistenceService, Console console) throws JdbcSQLException {
251 persistenceService.populateItemNameToTableNameMap();
252 console.println("Item index reloaded.");
256 public List<String> getUsages() {
257 return Arrays.asList(buildCommandUsage(CMD_SCHEMA + " " + SUBCMD_SCHEMA_CHECK, "check schema integrity"),
258 buildCommandUsage(CMD_SCHEMA + " " + SUBCMD_SCHEMA_FIX + " [<itemName>]", "fix schema integrity"),
259 buildCommandUsage(CMD_TABLES + " " + SUBCMD_TABLES_LIST + " [" + PARAMETER_ALL + "]",
260 "list tables (all = include valid)"),
262 CMD_TABLES + " " + SUBCMD_TABLES_CLEAN + " [<itemName>]" + " [" + PARAMETER_FORCE + "]",
263 "clean inconsistent items (remove from index and drop tables)"),
264 buildCommandUsage(CMD_RELOAD, "reload item index/schema"));
268 public @Nullable ConsoleCommandCompleter getCompleter() {
273 public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List<String> candidates) {
274 if (cursorArgumentIndex <= 0) {
275 return CMD_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates);
276 } else if (cursorArgumentIndex == 1) {
277 if (CMD_TABLES.equalsIgnoreCase(args[0])) {
278 return SUBCMD_TABLES_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates);
279 } else if (CMD_SCHEMA.equalsIgnoreCase(args[0])) {
280 return SUBCMD_SCHEMA_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates);
282 } else if (cursorArgumentIndex == 2) {
283 if (CMD_TABLES.equalsIgnoreCase(args[0])) {
284 if (SUBCMD_TABLES_CLEAN.equalsIgnoreCase(args[1])) {
285 JdbcPersistenceService persistenceService = getPersistenceService();
286 if (persistenceService != null) {
287 return new StringsCompleter(persistenceService.getItemNames(), true).complete(args,
288 cursorArgumentIndex, cursorPosition, candidates);
290 } else if (SUBCMD_TABLES_LIST.equalsIgnoreCase(args[1])) {
291 new StringsCompleter(List.of(PARAMETER_ALL), false).complete(args, cursorArgumentIndex,
292 cursorPosition, candidates);
294 } else if (CMD_SCHEMA.equalsIgnoreCase(args[0])) {
295 if (SUBCMD_SCHEMA_FIX.equalsIgnoreCase(args[1])) {
296 JdbcPersistenceService persistenceService = getPersistenceService();
297 if (persistenceService != null) {
298 return new StringsCompleter(persistenceService.getItemNames(), true).complete(args,
299 cursorArgumentIndex, cursorPosition, candidates);