This is an automated email from the ASF dual-hosted git repository.

nizhikov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/master by this push:
     new c7d2e6e6f12 IGNITE-22538 Customizable CacheScanTask output (#11400)
c7d2e6e6f12 is described below

commit c7d2e6e6f12990e0bcf2166bdef10f7a9de9c38f
Author: Nikolay <[email protected]>
AuthorDate: Thu Jun 20 18:09:02 2024 +0300

    IGNITE-22538 Customizable CacheScanTask output (#11400)
---
 .../util/GridCommandHandlerClusterByClassTest.java | 135 ++++++++++++++++++
 ...ernal.management.cache.scan.CacheScanTaskFormat |   2 +
 .../internal/management/cache/CacheCommand.java    |   1 +
 .../cache/{ => scan}/CacheScanCommand.java         |   2 +-
 .../cache/{ => scan}/CacheScanCommandArg.java      |  18 ++-
 .../management/cache/{ => scan}/CacheScanTask.java | 135 ++++--------------
 .../cache/scan/CacheScanTaskFormat.java}           |  44 +++---
 .../cache/{ => scan}/CacheScanTaskResult.java      |   2 +-
 .../cache/scan/DefaultCacheScanTaskFormat.java     | 153 +++++++++++++++++++++
 .../cache/scan/TableCacheScanTaskFormat.java       | 102 ++++++++++++++
 .../main/resources/META-INF/classnames.properties  |   8 +-
 .../org/apache/ignite/platform/model/Employee.java |  10 ++
 .../apache/ignite/testframework/GridTestUtils.java |  24 ++++
 ...mandHandlerClusterByClassTest_cache_help.output |   3 +-
 ...dlerClusterByClassWithSSLTest_cache_help.output |   3 +-
 15 files changed, 501 insertions(+), 141 deletions(-)

diff --git 
a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClusterByClassTest.java
 
b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClusterByClassTest.java
index 3747d5aa98a..13d380ddc6c 100644
--- 
a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClusterByClassTest.java
+++ 
b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClusterByClassTest.java
@@ -24,7 +24,9 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Calendar;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -80,6 +82,8 @@ import 
org.apache.ignite.internal.management.cache.CacheClearCommand;
 import org.apache.ignite.internal.management.cache.CacheCommand;
 import org.apache.ignite.internal.management.cache.CacheDestroyCommand;
 import org.apache.ignite.internal.management.cache.IdleVerifyDumpTask;
+import 
org.apache.ignite.internal.management.cache.scan.DefaultCacheScanTaskFormat;
+import 
org.apache.ignite.internal.management.cache.scan.TableCacheScanTaskFormat;
 import org.apache.ignite.internal.management.tx.TxTaskResult;
 import org.apache.ignite.internal.processors.cache.CacheGroupContext;
 import org.apache.ignite.internal.processors.cache.CacheType;
@@ -96,6 +100,9 @@ import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.lang.IgniteInClosure;
 import org.apache.ignite.logger.java.JavaLogger;
+import org.apache.ignite.platform.model.AccessLevel;
+import org.apache.ignite.platform.model.Employee;
+import org.apache.ignite.platform.model.Key;
 import org.apache.ignite.testframework.junits.GridAbstractTest;
 import org.apache.ignite.testframework.junits.WithSystemProperty;
 import org.apache.ignite.transactions.Transaction;
@@ -187,6 +194,18 @@ public class GridCommandHandlerClusterByClassTest extends 
GridCommandHandlerClus
     /** */
     public static final String SCAN = "scan";
 
+    /** */
+    public static final String JOHN = "John Connor";
+
+    /** */
+    public static final String SARAH = "Sarah Connor";
+
+    /** */
+    public static final String KYLE = "Kyle Reese";
+
+    /** */
+    public static final Date DATE = new Date(104, Calendar.JULY, 25);
+
     /**
      * Very basic tests for running the command in different environment which 
other command are running in.
      */
@@ -1656,6 +1675,87 @@ public class GridCommandHandlerClusterByClassTest 
extends GridCommandHandlerClus
         assertNotContains(log, testOut.toString(), "Result limited");
     }
 
+    /** */
+    private void dataForScanTest() {
+        IgniteCache<Key, Employee> c1 = crd.createCache(new 
CacheConfiguration<>("cache1"));
+
+        c1.put(new Key(1), new Employee(JOHN, 2));
+        c1.put(new Key(2), new Employee(SARAH, 3));
+        c1.put(new Key(3), new Employee(KYLE, 4));
+
+        IgniteCache<Integer, String> c2 = crd.createCache(new 
CacheConfiguration<>("cache2"));
+
+        c2.put(1, JOHN);
+        c2.put(2, SARAH);
+        c2.put(3, KYLE);
+
+        IgniteCache<Integer, TestClass2> c3 = crd.createCache(new 
CacheConfiguration<>("cache3"));
+
+        c3.put(1, new TestClass2(
+            1,
+            new int[]{2, 3},
+            Collections.singletonMap("some_key", "some_value"),
+            new String[] {"s1", "s2", "s3"},
+            DATE,
+            Arrays.asList(1, 2, 3),
+            AccessLevel.USER
+        ));
+
+        c3.put(2, new TestClass2(
+            2,
+            new int[]{3, 4},
+            Collections.singletonMap("1", "2"),
+            new String[] {"s4", "s5", "s6"},
+            DATE,
+            Arrays.asList(1, 2, 3),
+            AccessLevel.USER
+        ));
+
+        c3.put(3, new TestClass2(
+            3,
+            new int[]{4, 5},
+            Collections.singletonMap("xxx", "yyy"),
+            new String[] {"s7", "s8", "s9"},
+            DATE,
+            Arrays.asList(1, 2, 3),
+            AccessLevel.SUPER
+        ));
+    }
+
+    /** */
+    @Test
+    public void testCacheScanTableFormat() {
+        injectTestSystemOut();
+
+        autoConfirmation = false;
+
+        dataForScanTest();
+
+        assertEquals(EXIT_CODE_OK, execute("--cache", SCAN, "--output-format", 
TableCacheScanTaskFormat.NAME, "cache1"));
+
+        assertContains(log, testOut.toString(), Pattern.compile("id *fio 
*salary *\n"));
+        assertContains(log, testOut.toString(), Pattern.compile("1 *" + JOHN + 
" *2 *\n"));
+        assertContains(log, testOut.toString(), Pattern.compile("2 *" + SARAH 
+ " *3 *\n"));
+        assertContains(log, testOut.toString(), Pattern.compile("3 *" + KYLE + 
" *4 *\n"));
+
+        assertEquals(EXIT_CODE_OK, execute("--cache", SCAN, "--output-format", 
TableCacheScanTaskFormat.NAME, "cache2"));
+
+        assertContains(
+            log,
+            testOut.toString(),
+            Pattern.compile(DefaultCacheScanTaskFormat.KEY + " *" + 
DefaultCacheScanTaskFormat.VALUE + " *\n")
+        );
+        assertContains(log, testOut.toString(), Pattern.compile("1 *" + JOHN + 
" *\n"));
+        assertContains(log, testOut.toString(), Pattern.compile("2 *" + SARAH 
+ " *\n"));
+        assertContains(log, testOut.toString(), Pattern.compile("3 *" + KYLE + 
" *\n"));
+
+        assertEquals(EXIT_CODE_OK, execute("--cache", SCAN, "--output-format", 
TableCacheScanTaskFormat.NAME, "cache3"));
+
+        assertContains(log, testOut.toString(), "some_key=some_value");
+        assertContains(log, testOut.toString(), "xxx=yyy");
+        assertContains(log, testOut.toString(), DATE.toString());
+    }
+
     /** */
     @Test
     public void testCacheScanLimit() {
@@ -2272,4 +2372,39 @@ public class GridCommandHandlerClusterByClassTest 
extends GridCommandHandlerClus
             this.s = s;
         }
     }
+
+    /** */
+    private static class TestClass2 {
+        /** */
+        private final int i;
+
+        /** */
+        private final int[] ints;
+
+        /** */
+        private final Map<?, ?> map;
+
+        /** */
+        private final String[] strArr;
+
+        /** */
+        private final Date date;
+
+        /** */
+        private final List<?> list;
+
+        /** */
+        private final AccessLevel enm;
+
+        /** */
+        public TestClass2(int i, int[] ints, Map<?, ?> map, String[] strArr, 
Date date, List<?> list, AccessLevel enm) {
+            this.i = i;
+            this.ints = ints;
+            this.map = map;
+            this.strArr = strArr;
+            this.date = date;
+            this.list = list;
+            this.enm = enm;
+        }
+    }
 }
diff --git 
a/modules/core/src/main/java/META-INF/services/org.apache.ignite.internal.management.cache.scan.CacheScanTaskFormat
 
b/modules/core/src/main/java/META-INF/services/org.apache.ignite.internal.management.cache.scan.CacheScanTaskFormat
new file mode 100644
index 00000000000..76b16f3df03
--- /dev/null
+++ 
b/modules/core/src/main/java/META-INF/services/org.apache.ignite.internal.management.cache.scan.CacheScanTaskFormat
@@ -0,0 +1,2 @@
+org.apache.ignite.internal.management.cache.scan.DefaultCacheScanTaskFormat
+org.apache.ignite.internal.management.cache.scan.TableCacheScanTaskFormat
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheCommand.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheCommand.java
index 5720f8d19e5..66517db7c60 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheCommand.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheCommand.java
@@ -20,6 +20,7 @@ package org.apache.ignite.internal.management.cache;
 import org.apache.ignite.internal.management.api.CommandRegistryImpl;
 import org.apache.ignite.internal.management.api.HelpCommand;
 import org.apache.ignite.internal.management.api.NoArg;
+import org.apache.ignite.internal.management.cache.scan.CacheScanCommand;
 
 /** */
 public class CacheCommand extends CommandRegistryImpl<NoArg, Void> {
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheScanCommand.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/CacheScanCommand.java
similarity index 97%
rename from 
modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheScanCommand.java
rename to 
modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/CacheScanCommand.java
index 7ccf9e790dd..13022685194 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheScanCommand.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/CacheScanCommand.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.management.cache;
+package org.apache.ignite.internal.management.cache.scan;
 
 import java.util.List;
 import java.util.function.Consumer;
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheScanCommandArg.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/CacheScanCommandArg.java
similarity index 82%
rename from 
modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheScanCommandArg.java
rename to 
modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/CacheScanCommandArg.java
index d39df18f14f..e1278c95e5b 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheScanCommandArg.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/CacheScanCommandArg.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.management.cache;
+package org.apache.ignite.internal.management.cache.scan;
 
 import java.io.IOException;
 import java.io.ObjectInput;
@@ -38,6 +38,10 @@ public class CacheScanCommandArg extends 
IgniteDataTransferObject {
     @Argument(example = "cacheName")
     private String cacheName;
 
+    /** */
+    @Argument(description = "Pluggable output format. 'default', 'table' 
exists by default", example = "table", optional = true)
+    private String outputFormat;
+
     /** */
     @Argument(description = "limit count of entries to scan (" + DFLT_LIMIT + 
" by default)", example = "N", optional = true)
     private int limit = DFLT_LIMIT;
@@ -45,15 +49,27 @@ public class CacheScanCommandArg extends 
IgniteDataTransferObject {
     /** {@inheritDoc} */
     @Override protected void writeExternalData(ObjectOutput out) throws 
IOException {
         U.writeString(out, cacheName);
+        U.writeString(out, outputFormat);
         out.writeInt(limit);
     }
 
     /** {@inheritDoc} */
     @Override protected void readExternalData(byte protoVer, ObjectInput in) 
throws IOException, ClassNotFoundException {
         cacheName = U.readString(in);
+        outputFormat = U.readString(in);
         limit = in.readInt();
     }
 
+    /** */
+    public String outputFormat() {
+        return outputFormat;
+    }
+
+    /** */
+    public void outputFormat(String format) {
+        this.outputFormat = format;
+    }
+
     /** */
     public String cacheName() {
         return cacheName;
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheScanTask.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/CacheScanTask.java
similarity index 51%
rename from 
modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheScanTask.java
rename to 
modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/CacheScanTask.java
index 646693ed92e..ca8b5ba8dda 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheScanTask.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/CacheScanTask.java
@@ -15,25 +15,20 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.management.cache;
+package org.apache.ignite.internal.management.cache.scan;
 
 import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import javax.cache.Cache;
 import org.apache.ignite.IgniteCache;
-import org.apache.ignite.binary.BinaryObject;
-import org.apache.ignite.binary.BinaryObjectException;
-import org.apache.ignite.binary.BinaryType;
 import org.apache.ignite.cache.query.QueryCursor;
 import org.apache.ignite.cache.query.ScanQuery;
-import org.apache.ignite.internal.binary.BinaryObjectEx;
 import org.apache.ignite.internal.processors.task.GridInternal;
-import org.apache.ignite.internal.util.IgniteUtils;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.S;
-import org.apache.ignite.internal.util.typedef.internal.SB;
+import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.internal.visor.VisorJob;
 import org.apache.ignite.internal.visor.VisorOneNodeTask;
 import org.apache.ignite.plugin.security.SecurityPermissionSet;
@@ -82,23 +77,28 @@ public class CacheScanTask extends 
VisorOneNodeTask<CacheScanCommandArg, CacheSc
 
             IgniteCache<Object, Object> cache = 
ignite.cache(arg.cacheName()).withKeepBinary();
 
-            List<String> titles = Arrays.asList("Key Class", "Key", "Value 
Class", "Value");
-
             int cnt = 0;
             List<List<?>> entries = new ArrayList<>();
 
+            CacheScanTaskFormat format = format(arg.outputFormat());
+
+            List<String> titles = Collections.emptyList();
+
             ScanQuery<Object, Object> scanQry = new 
ScanQuery<>().setPageSize(min(arg.limit(), DFLT_PAGE_SIZE));
 
             try (QueryCursor<Cache.Entry<Object, Object>> qry = 
cache.query(scanQry)) {
                 Iterator<Cache.Entry<Object, Object>> iter = qry.iterator();
 
-                while (cnt++ < arg.limit() && iter.hasNext()) {
-                    Cache.Entry<Object, Object> next = iter.next();
+                if (cnt++ < arg.limit() && iter.hasNext()) {
+                    Cache.Entry<Object, Object> first = iter.next();
+
+                    titles = format.titles(first);
 
-                    Object k = next.getKey();
-                    Object v = next.getValue();
+                    entries.add(format.row(first));
+                }
 
-                    entries.add(Arrays.asList(typeOf(k), valueOf(k), 
typeOf(v), valueOf(v)));
+                while (cnt++ < arg.limit() && iter.hasNext()) {
+                    entries.add(format.row(iter.next()));
                 }
             }
 
@@ -112,103 +112,24 @@ public class CacheScanTask extends 
VisorOneNodeTask<CacheScanCommandArg, CacheSc
             return NO_PERMISSIONS;
         }
 
-        /**
-         * @param o Source object.
-         * @return String representation of object class.
-         */
-        private static String typeOf(Object o) {
-            if (o != null) {
-                Class<?> clazz = o.getClass();
-
-                return clazz.isArray() ? 
IgniteUtils.compact(clazz.getComponentType().getName()) + "[]"
-                    : IgniteUtils.compact(o.getClass().getName());
-            }
-            else
-                return "n/a";
-        }
-
-        /**
-         * @param o Object.
-         * @return String representation of value.
-         */
-        private static String valueOf(Object o) {
-            if (o == null)
-                return "null";
-
-            if (o instanceof byte[])
-                return "size=" + ((byte[])o).length;
-
-            if (o instanceof Byte[])
-                return "size=" + ((Byte[])o).length;
-
-            if (o instanceof Object[]) {
-                return "size=" + ((Object[])o).length +
-                    ", values=[" + S.joinToString(Arrays.asList((Object[])o), 
", ", "...", 120, 0) + "]";
-            }
-
-            if (o instanceof BinaryObject)
-                return binaryToString((BinaryObject)o);
-
-            return o.toString();
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return S.toString(CacheScanJob.class, this);
         }
+    }
 
-        /**
-         * Convert Binary object to string.
-         *
-         * @param obj Binary object.
-         * @return String representation of Binary object.
-         */
-        public static String binaryToString(BinaryObject obj) {
-            int hash = obj.hashCode();
-
-            if (obj instanceof BinaryObjectEx) {
-                BinaryObjectEx objEx = (BinaryObjectEx)obj;
-
-                BinaryType meta;
-
-                try {
-                    meta = ((BinaryObjectEx)obj).rawType();
-                }
-                catch (BinaryObjectException ignore) {
-                    meta = null;
-                }
-
-                if (meta != null) {
-                    if (meta.isEnum()) {
-                        try {
-                            return obj.deserialize().toString();
-                        }
-                        catch (BinaryObjectException ignore) {
-                            // NO-op.
-                        }
-                    }
-
-                    SB buf = new SB(meta.typeName());
-
-                    if (meta.fieldNames() != null) {
-                        buf.a(" [hash=").a(hash);
-
-                        for (String name : meta.fieldNames()) {
-                            Object val = objEx.field(name);
-
-                            buf.a(", ").a(name).a('=').a(val);
-                        }
+    /** */
+    private static CacheScanTaskFormat format(String format) {
+        if (format == null)
+            return new DefaultCacheScanTaskFormat();
 
-                        buf.a(']');
+        Iterable<CacheScanTaskFormat> formats = 
U.loadService(CacheScanTaskFormat.class);
 
-                        return buf.toString();
-                    }
-                }
-            }
-
-            return S.toString(obj.getClass().getSimpleName(),
-                "hash", hash, false,
-                "typeId", obj.type().typeId(), true);
+        for (CacheScanTaskFormat f : formats) {
+            if (f.name().equals(format))
+                return f;
         }
 
-        /** {@inheritDoc} */
-        @Override public String toString() {
-            return S.toString(CacheScanJob.class, this);
-        }
+        throw new IllegalStateException("Unknown format: " + format);
     }
 }
diff --git 
a/modules/core/src/test/java/org/apache/ignite/platform/model/Employee.java 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/CacheScanTaskFormat.java
similarity index 57%
copy from 
modules/core/src/test/java/org/apache/ignite/platform/model/Employee.java
copy to 
modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/CacheScanTaskFormat.java
index 6c63c29d684..a4e4ed8f02f 100644
--- a/modules/core/src/test/java/org/apache/ignite/platform/model/Employee.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/CacheScanTaskFormat.java
@@ -15,33 +15,27 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.platform.model;
+package org.apache.ignite.internal.management.cache.scan;
 
-/** Test value object. */
-public class Employee {
-    /** */
-    private String fio;
+import java.util.List;
+import javax.cache.Cache;
 
-    /** */
-    private long salary;
-
-    /** */
-    public String getFio() {
-        return fio;
-    }
-
-    /** */
-    public void setFio(String fio) {
-        this.fio = fio;
-    }
+/**
+ * This is pluggable interface to customize string representation of cache 
data.
+ */
+public interface CacheScanTaskFormat {
+    /**
+     * @return name of the format.
+     * @see CacheScanCommandArg
+     */
+    String name();
 
-    /** */
-    public long getSalary() {
-        return salary;
-    }
+    /**
+     * Calculates and returns titles based on first cache entry.
+     * @return Column titles.
+     */
+    List<String> titles(Cache.Entry<Object, Object> first);
 
-    /** */
-    public void setSalary(long salary) {
-        this.salary = salary;
-    }
+    /** Row for single cache entry. */
+    List<?> row(Cache.Entry<Object, Object> e);
 }
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheScanTaskResult.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/CacheScanTaskResult.java
similarity index 97%
rename from 
modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheScanTaskResult.java
rename to 
modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/CacheScanTaskResult.java
index 9a3a8b41354..29abbf1bbb0 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheScanTaskResult.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/CacheScanTaskResult.java
@@ -15,7 +15,7 @@
 * limitations under the License.
 */
 
-package org.apache.ignite.internal.management.cache;
+package org.apache.ignite.internal.management.cache.scan;
 
 import java.io.IOException;
 import java.io.ObjectInput;
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/DefaultCacheScanTaskFormat.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/DefaultCacheScanTaskFormat.java
new file mode 100644
index 00000000000..f6aa82b0fa8
--- /dev/null
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/DefaultCacheScanTaskFormat.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.ignite.internal.management.cache.scan;
+
+import java.util.Arrays;
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectException;
+import org.apache.ignite.binary.BinaryType;
+import org.apache.ignite.internal.binary.BinaryObjectEx;
+import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.internal.util.typedef.internal.S;
+import org.apache.ignite.internal.util.typedef.internal.SB;
+
+/**
+ * Default cache scan task format.
+ */
+public class DefaultCacheScanTaskFormat implements CacheScanTaskFormat {
+    /** Key title. */
+    public static final String KEY = "Key";
+
+    /** Value title. */
+    public static final String VALUE = "Value";
+
+    /** {@inheritDoc} */
+    @Override public String name() {
+        return "default";
+    }
+
+    /** {@inheritDoc} */
+    @Override public List<String> titles(Cache.Entry<Object, Object> first) {
+        return Arrays.asList("Key Class", KEY, "Value Class", VALUE);
+    }
+
+    /** {@inheritDoc} */
+    @Override public List<?> row(Cache.Entry<Object, Object> e) {
+        Object k = e.getKey();
+        Object v = e.getValue();
+
+        return Arrays.asList(typeOf(k), valueOf(k), typeOf(v), valueOf(v));
+    }
+
+    /**
+     * @param o Source object.
+     * @return String representation of object class.
+     */
+    private static String typeOf(Object o) {
+        if (o != null) {
+            Class<?> clazz = o.getClass();
+
+            return clazz.isArray() ? 
IgniteUtils.compact(clazz.getComponentType().getName()) + "[]"
+                : IgniteUtils.compact(o.getClass().getName());
+        }
+        else
+            return "n/a";
+    }
+
+    /**
+     * @param o Object.
+     * @return String representation of value.
+     */
+    static String valueOf(Object o) {
+        if (o == null)
+            return "null";
+
+        if (o instanceof byte[])
+            return "size=" + ((byte[])o).length;
+
+        if (o instanceof Byte[])
+            return "size=" + ((Byte[])o).length;
+
+        if (o instanceof Object[]) {
+            return "size=" + ((Object[])o).length +
+                ", values=[" + S.joinToString(Arrays.asList((Object[])o), ", 
", "...", 120, 0) + "]";
+        }
+
+        if (o instanceof BinaryObject)
+            return binaryToString((BinaryObject)o);
+
+        return o.toString();
+    }
+
+    /**
+     * Convert Binary object to string.
+     *
+     * @param obj Binary object.
+     * @return String representation of Binary object.
+     */
+    public static String binaryToString(BinaryObject obj) {
+        int hash = obj.hashCode();
+
+        if (obj instanceof BinaryObjectEx) {
+            BinaryObjectEx objEx = (BinaryObjectEx)obj;
+
+            BinaryType meta;
+
+            try {
+                meta = ((BinaryObjectEx)obj).rawType();
+            }
+            catch (BinaryObjectException ignore) {
+                meta = null;
+            }
+
+            if (meta != null) {
+                if (meta.isEnum()) {
+                    try {
+                        return obj.deserialize().toString();
+                    }
+                    catch (BinaryObjectException ignore) {
+                        // NO-op.
+                    }
+                }
+
+                SB buf = new SB(meta.typeName());
+
+                if (meta.fieldNames() != null) {
+                    buf.a(" [hash=").a(hash);
+
+                    for (String name : meta.fieldNames()) {
+                        Object val = objEx.field(name);
+
+                        buf.a(", ").a(name).a('=').a(val);
+                    }
+
+                    buf.a(']');
+
+                    return buf.toString();
+                }
+            }
+        }
+
+        return S.toString(obj.getClass().getSimpleName(),
+            "hash", hash, false,
+            "typeId", obj.type().typeId(), true);
+    }
+
+}
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/TableCacheScanTaskFormat.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/TableCacheScanTaskFormat.java
new file mode 100644
index 00000000000..bf754225f68
--- /dev/null
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/scan/TableCacheScanTaskFormat.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.ignite.internal.management.cache.scan;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.binary.BinaryObject;
+
+/**
+ * This format prints cache objects fields in table format.
+ */
+public class TableCacheScanTaskFormat implements CacheScanTaskFormat {
+    /** */
+    public static final String NAME = "table";
+
+    /** */
+    private List<String> keyTitles;
+
+    /** */
+    private List<String> valTitles;
+
+    /** {@inheritDoc} */
+    @Override public String name() {
+        return NAME;
+    }
+
+    /** {@inheritDoc} */
+    @Override public List<String> titles(Cache.Entry<Object, Object> first) {
+        keyTitles = titles(first.getKey(), DefaultCacheScanTaskFormat.KEY);
+        valTitles = titles(first.getValue(), DefaultCacheScanTaskFormat.VALUE);
+
+        List<String> res = new ArrayList<>(keyTitles.size() + 
valTitles.size());
+
+        res.addAll(keyTitles);
+        res.addAll(valTitles);
+
+        return res;
+    }
+
+    /** {@inheritDoc} */
+    @Override public List<?> row(Cache.Entry<Object, Object> e) {
+        List<String> res = new ArrayList<>(keyTitles.size() + 
valTitles.size());
+
+        res.addAll(columns(e.getKey(), keyTitles, 
DefaultCacheScanTaskFormat.KEY));
+        res.addAll(columns(e.getValue(), valTitles, 
DefaultCacheScanTaskFormat.VALUE));
+
+        return res;
+    }
+
+    /** */
+    private List<String> titles(Object o, String dflt) {
+        if (o instanceof BinaryObject) {
+            BinaryObject b = (BinaryObject)o;
+
+            List<String> flds = new ArrayList<>(b.type().fieldNames());
+
+            Collections.sort(flds);
+
+            return flds;
+        }
+
+        return Collections.singletonList(dflt);
+    }
+
+    /** */
+    private Collection<String> columns(Object o, List<String> titles, String 
dfltTitle) {
+        if (titles.size() == 1 && titles.get(0).equals(dfltTitle))
+            return 
Collections.singletonList(DefaultCacheScanTaskFormat.valueOf(o));
+
+        String[] res = new String[titles.size()];
+
+        if (o instanceof BinaryObject) {
+            BinaryObject b = (BinaryObject)o;
+
+            for (int i = 0; i < titles.size(); i++) {
+                if (b.hasField(titles.get(i)))
+                    res[i] = 
DefaultCacheScanTaskFormat.valueOf(b.field(titles.get(i)));
+            }
+        }
+
+        return Arrays.asList(res);
+    }
+}
diff --git a/modules/core/src/main/resources/META-INF/classnames.properties 
b/modules/core/src/main/resources/META-INF/classnames.properties
index e4544198795..7a45d5841c8 100644
--- a/modules/core/src/main/resources/META-INF/classnames.properties
+++ b/modules/core/src/main/resources/META-INF/classnames.properties
@@ -441,7 +441,7 @@ 
org.apache.ignite.internal.management.cache.CacheListCommand$OutputFormat
 org.apache.ignite.internal.management.cache.CacheListCommandArg
 org.apache.ignite.internal.management.cache.CacheMetricsCommandArg
 org.apache.ignite.internal.management.cache.CacheResetLostPartitionsCommandArg
-org.apache.ignite.internal.management.cache.CacheScanCommandArg
+org.apache.ignite.internal.management.cache.scan.CacheScanCommandArg
 
org.apache.ignite.internal.management.cache.CacheScheduleIndexesRebuildCommandArg
 org.apache.ignite.internal.management.cache.CacheValidateIndexesCommandArg
 org.apache.ignite.internal.management.cdc.CdcDeleteLostSegmentLinksCommandArg
@@ -2095,9 +2095,9 @@ org.apache.ignite.internal.management.cache.CacheJdbcType
 org.apache.ignite.internal.management.cache.CacheJdbcTypeField
 org.apache.ignite.internal.management.cache.CacheNearConfiguration
 org.apache.ignite.internal.management.cache.CacheRebalanceConfiguration
-org.apache.ignite.internal.management.cache.CacheScanTask
-org.apache.ignite.internal.management.cache.CacheScanTask$CacheScanJob
-org.apache.ignite.internal.management.cache.CacheScanTaskResult
+org.apache.ignite.internal.management.cache.scan.CacheScanTask
+org.apache.ignite.internal.management.cache.scan.CacheScanTask$CacheScanJob
+org.apache.ignite.internal.management.cache.scan.CacheScanTaskResult
 org.apache.ignite.internal.management.cache.CacheStopTask
 org.apache.ignite.internal.management.cache.CacheStopTask$CacheStopJob
 org.apache.ignite.internal.management.cache.CacheStoreConfiguration
diff --git 
a/modules/core/src/test/java/org/apache/ignite/platform/model/Employee.java 
b/modules/core/src/test/java/org/apache/ignite/platform/model/Employee.java
index 6c63c29d684..687a0bcdc1e 100644
--- a/modules/core/src/test/java/org/apache/ignite/platform/model/Employee.java
+++ b/modules/core/src/test/java/org/apache/ignite/platform/model/Employee.java
@@ -25,6 +25,16 @@ public class Employee {
     /** */
     private long salary;
 
+    /** */
+    public Employee() {
+    }
+
+    /** */
+    public Employee(String fio, long salary) {
+        this.fio = fio;
+        this.salary = salary;
+    }
+
     /** */
     public String getFio() {
         return fio;
diff --git 
a/modules/core/src/test/java/org/apache/ignite/testframework/GridTestUtils.java 
b/modules/core/src/test/java/org/apache/ignite/testframework/GridTestUtils.java
index b0ce812fb37..3e5000c6469 100644
--- 
a/modules/core/src/test/java/org/apache/ignite/testframework/GridTestUtils.java
+++ 
b/modules/core/src/test/java/org/apache/ignite/testframework/GridTestUtils.java
@@ -68,6 +68,8 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.BooleanSupplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import javax.cache.CacheException;
@@ -411,6 +413,28 @@ public final class GridTestUtils {
         }
     }
 
+    /**
+     * Checks that string {@param str} matches {@param pattern}. Logs both 
strings
+     * and throws {@link java.lang.AssertionError}, if not.
+     *
+     * @param log Logger (optional).
+     * @param str String.
+     * @param pattern Pattern.
+     */
+    public static void assertContains(@Nullable IgniteLogger log, String str, 
Pattern pattern) {
+        try {
+            Matcher m = pattern.matcher(str);
+            assertTrue(str != null && m.find());
+        }
+        catch (AssertionError e) {
+            U.warn(log, String.format("String does not match pattern: '%s':", 
pattern));
+            U.warn(log, "String:");
+            U.warn(log, str);
+
+            throw e;
+        }
+    }
+
     /**
      * Checks that collection {@param col} contains element {@param elem}. 
Logs collection, element
      * and throws {@link java.lang.AssertionError}, if not.
diff --git 
a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_cache_help.output
 
b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_cache_help.output
index 6541838e748..db5136e1668 100644
--- 
a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_cache_help.output
+++ 
b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_cache_help.output
@@ -141,9 +141,10 @@ Arguments: --cache help --yes
       --group-names groupName1,groupName2,...groupNameN                        
- Comma-separated list of cache group names for which indexes should be 
scheduled for the rebuild. Can be used simultaneously with cache names.
 
   Show cache content:
-    control.(sh|bat) --cache scan cacheName [--limit N]
+    control.(sh|bat) --cache scan cacheName [--output-format table] [--limit N]
 
     Parameters:
+      --output-format table  - Pluggable output format. 'default', 'table' 
exists by default.
       --limit N  - limit count of entries to scan (1000 by default).
 
 Command [CACHE] finished with code: 0
diff --git 
a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_cache_help.output
 
b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_cache_help.output
index 6541838e748..db5136e1668 100644
--- 
a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_cache_help.output
+++ 
b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_cache_help.output
@@ -141,9 +141,10 @@ Arguments: --cache help --yes
       --group-names groupName1,groupName2,...groupNameN                        
- Comma-separated list of cache group names for which indexes should be 
scheduled for the rebuild. Can be used simultaneously with cache names.
 
   Show cache content:
-    control.(sh|bat) --cache scan cacheName [--limit N]
+    control.(sh|bat) --cache scan cacheName [--output-format table] [--limit N]
 
     Parameters:
+      --output-format table  - Pluggable output format. 'default', 'table' 
exists by default.
       --limit N  - limit count of entries to scan (1000 by default).
 
 Command [CACHE] finished with code: 0


Reply via email to