/* * JBoss, Home of Professional Open Source. * See the COPYRIGHT.txt file distributed with this work for information * regarding copyright ownership. Some portions may be licensed * to Red Hat, Inc. under one or more contributor license agreements. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. */ package org.teiid.translator.couchbase; import static org.teiid.language.SQLConstants.Tokens.*; import static org.teiid.metadata.BaseColumn.NullType.*; import static org.teiid.translator.TypeFacility.RUNTIME_NAMES.*; import static org.teiid.translator.couchbase.CouchbaseProperties.*; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.teiid.core.types.DataTypeManager; import org.teiid.couchbase.CouchbaseConnection; import org.teiid.logging.LogConstants; import org.teiid.logging.LogManager; import org.teiid.metadata.Column; import org.teiid.metadata.Datatype; import org.teiid.metadata.ExtensionMetadataProperty; import org.teiid.metadata.MetadataFactory; import org.teiid.metadata.Procedure; import org.teiid.metadata.ProcedureParameter; import org.teiid.metadata.ProcedureParameter.Type; import org.teiid.metadata.Table; import org.teiid.translator.MetadataProcessor; import org.teiid.translator.TranslatorException; import org.teiid.translator.TranslatorProperty; import org.teiid.translator.TypeFacility; import org.teiid.translator.TranslatorProperty.PropertyType; import com.couchbase.client.java.document.json.JsonArray; import com.couchbase.client.java.document.json.JsonObject; import com.couchbase.client.java.document.json.JsonValue; import com.couchbase.client.java.query.N1qlQueryRow; public class CouchbaseMetadataProcessor implements MetadataProcessor<CouchbaseConnection> { @ExtensionMetadataProperty(applicable = Table.class, datatype = Boolean.class, display = "Is Array Table", description = "Declare whether the table is array table") public static final String IS_ARRAY_TABLE = MetadataFactory.COUCHBASE_URI + "ISARRAYTABLE"; //$NON-NLS-1$ @ExtensionMetadataProperty(applicable = Table.class, datatype = String.class, display = "Named Type Pair", description = "Declare the name of typed key/value pair") public static final String NAMED_TYPE_PAIR = MetadataFactory.COUCHBASE_URI + "NAMEDTYPEPAIR"; //$NON-NLS-1$ private Integer sampleSize; private String typeNameList; private Map<String, String> typeNameMap; @Override public void process(MetadataFactory mf, CouchbaseConnection conn) throws TranslatorException { List<String> keyspaces = loadKeyspaces(conn); for(String keyspace : keyspaces) { addTable(mf, conn, conn.getNamespace(), keyspace); } addProcedures(mf, conn); } private List<String> loadKeyspaces(CouchbaseConnection conn) { String namespace = conn.getNamespace(); boolean isValidSchema = false; String n1qlNamespaces = buildN1QLNamespaces(); Iterator<N1qlQueryRow> namespaces = conn.executeQuery(n1qlNamespaces).iterator(); while(namespaces.hasNext()) { JsonObject row = namespaces.next().value(); if(row.getString(NAME).equals(namespace)){ isValidSchema = true; break; } } if (!isValidSchema) { LogManager.logDetail(LogConstants.CTX_CONNECTOR, CouchbasePlugin.Util.gs(CouchbasePlugin.Event.TEIID29010, DEFAULT_NAMESPACE)); namespace = DEFAULT_NAMESPACE; } List<String> results = new ArrayList<>(); String n1qlKeyspaces = buildN1QLKeyspaces(namespace); List<N1qlQueryRow> keyspaces = conn.executeQuery(n1qlKeyspaces).allRows(); for(N1qlQueryRow row : keyspaces){ String keyspace = row.value().getString(NAME); results.add(keyspace); } Collections.sort(results); LogManager.logDetail(LogConstants.CTX_CONNECTOR, CouchbasePlugin.Util.gs(CouchbasePlugin.Event.TEIID29011, n1qlKeyspaces, results)); return results; } /** * Basically, a keyspace be map to a table, keyspace name is the table name, if TranslatorProperty TypeNameList defined, * a keyspace may map to several tables, for example, if the TypeNameList=`default`:`type`, * then the {@link CouchbaseMetadataProcessor#addTable(MetadataFactory, CouchbaseConnection, namespace, namespace)} * will get all distinct `type` attribute referenced values from keyspace, and use all these values as table name. * * If multiple keyspaces has same typed value, for example, like TypeNameList=`default`:`type`,`default2`:`type`, both default and default2 * has document defined {"type": "Customer"}, then the default's table name is 'Customer', default2's table name is 'default2_Customer'. * * Scan row will add columns to table or create sub-table, nested array be map to a separated table. * * @param mf - MetadataFactory * @param conn - CouchbaseConnection * @param namespace - couchbase namespace * @param keyspace - couchbase keyspace */ private void addTable(MetadataFactory mf, CouchbaseConnection conn, String namespace, String keyspace) { String nameInSource = nameInSource(keyspace); String typeName = getTypeName(nameInSource); List<String> dataSrcTableList = new ArrayList<>(); if(typeName != null) { String typeQuery = buildN1QLTypeQuery(typeName, namespace, keyspace); LogManager.logTrace(LogConstants.CTX_CONNECTOR, CouchbasePlugin.Util.gs(CouchbasePlugin.Event.TEIID29003, typeQuery)); List<N1qlQueryRow> rows = conn.executeQuery(typeQuery).allRows(); for(N1qlQueryRow row : rows) { JsonObject rowJson = row.value(); String type = trimWave(typeName); String value = rowJson.getString(type); if(value != null) { dataSrcTableList.add(value); } } } else { dataSrcTableList.add(keyspace); } for(String name : dataSrcTableList) { String tableName = name; if (mf.getSchema().getTable(name) != null && !name.equals(keyspace)) { // handle multiple keyspaces has same typed table name tableName = keyspace + UNDERSCORE + name; } Table table = mf.addTable(tableName); table.setNameInSource(nameInSource); table.setSupportsUpdate(true); table.setProperty(IS_ARRAY_TABLE, FALSE_VALUE); Column column = mf.addColumn(DOCUMENTID, STRING, table); column.setUpdatable(true); mf.addPrimaryKey("PK0", Arrays.asList(DOCUMENTID), table); //$NON-NLS-1$ if(!name.equals(keyspace)) { String namedTypePair = buildNamedTypePair(typeName, name); table.setProperty(NAMED_TYPE_PAIR, namedTypePair); } // scan row boolean hasTypeIdentifier = true; if(dataSrcTableList.size() == 1 && dataSrcTableList.get(0).equals(keyspace)) { hasTypeIdentifier = false; } if(this.sampleSize == null || this.sampleSize == 0) { this.sampleSize = 100; // default sample size is 100 LogManager.logInfo(LogConstants.CTX_CONNECTOR, CouchbasePlugin.Util.gs(CouchbasePlugin.Event.TEIID29008, this.sampleSize)); } String query = buildN1QLQuery(typeName, name, namespace, keyspace, this.sampleSize, hasTypeIdentifier); LogManager.logTrace(LogConstants.CTX_CONNECTOR, CouchbasePlugin.Util.gs(CouchbasePlugin.Event.TEIID29003, query)); Iterator<N1qlQueryRow> result = conn.executeQuery(query).iterator(); while(result.hasNext()) { JsonObject row = result.next().value(); // result.next() always can not be null JsonObject currentRowJson = row.getObject(keyspace); scanRow(keyspace, nameInSource(keyspace), currentRowJson, mf, table, table.getName(), false, new Dimension()); } } } /** * A dispatcher of scan jsonValue(document, of a segment of document), the jsonValue either can be a JsonObject, or JsonArray, * different type dispatch to different scan method. * * @param key - The attribute name in document, which mapped with value * @param value - JsonObject/JsonArray which may contain nested JsonObject/JsonArray * @param mf * @param conn * @param table * @param referenceTableName - The top table name, used to add foreign key * @param isNestedType - Whether the jsonValue are a nested value, or the jsonValue is a segment of document * @param dimension - The dimension of nested array, for example, "{"nestedArray": [[["nestedArray"]]]}", the dimension * deepest array is 3 */ protected void scanRow(String key, String keyInSource, JsonValue value, MetadataFactory mf, Table table, String referenceTableName, boolean isNestedType, Dimension dimension) { LogManager.logTrace(LogConstants.CTX_CONNECTOR, CouchbasePlugin.Util.gs(CouchbasePlugin.Event.TEIID29013, table, key, value)); if(isObjectJsonType(value)) { scanObjectRow(key, keyInSource, (JsonObject)value, mf, table, referenceTableName, isNestedType, dimension); } else if (isArrayJsonType(value)) { scanArrayRow(key, keyInSource, (JsonArray)value, mf, table, referenceTableName, isNestedType, dimension); } } private void scanObjectRow(String key, String keyInSource, JsonObject value, MetadataFactory mf, Table table, String referenceTableName, boolean isNestedType, Dimension dimension) { Set<String> names = value.getNames(); for(String name : names) { String columnName = name; Object columnValue = value.get(columnName); String columnType = getDataType(columnValue); if(columnType.equals(OBJECT)) { JsonValue jsonValue = (JsonValue) columnValue; String newKey = key + UNDERSCORE + columnName; String newKeyInSource = keyInSource + SOURCE_SEPARATOR + this.nameInSource(columnName); if(isObjectJsonType(columnValue)) { scanRow(newKey, newKeyInSource, jsonValue, mf, table, referenceTableName, true, dimension); } else if(isArrayJsonType(columnValue)) { String tableName = repleaceTypedName(table.getName(), newKey); String tableNameInSource = newKeyInSource + SQUARE_BRACKETS ; Table subTable = addTable(tableName, tableNameInSource, true, referenceTableName, dimension, mf); scanRow(newKey, newKeyInSource, jsonValue, mf, subTable, referenceTableName, true, dimension); } } else { if(isNestedType) { columnName = key + UNDERSCORE + columnName; } String columnNameInSource = keyInSource + SOURCE_SEPARATOR +nameInSource(name); addColumn(columnName, columnType, columnValue, true, columnNameInSource, table, mf); } } } private void scanArrayRow(String keyspace, String keyInSource, JsonArray array, MetadataFactory mf, Table table, String referenceTableName, boolean isNestedType, Dimension dimension) { if(array.size() > 0) { for(int i = 0 ; i < array.size() ; i ++) { Object element = array.get(i); if(isObjectJsonType(element)) { JsonObject json = (JsonObject) element; for(String name : json.getNames()) { Object columnValue = json.get(name); String columnType = this.getDataType(columnValue); if(columnType.equals(OBJECT)) { JsonValue jsonValue = (JsonValue) columnValue; if(isObjectJsonType(jsonValue)) { scanRow(keyspace, keyInSource, jsonValue, mf, table, referenceTableName, true, dimension); } else if (isArrayJsonType(jsonValue)) { String tableName = table.getName() + UNDERSCORE + name + UNDERSCORE + dimension.get(); String tableNameInSrc = table.getNameInSource() + SOURCE_SEPARATOR + this.nameInSource(name) + SQUARE_BRACKETS; Table subTable = addTable(tableName, tableNameInSrc, true, referenceTableName, dimension, mf); scanRow(keyspace, keyInSource, jsonValue, mf, subTable, referenceTableName, true, dimension); } } else { String columnName = table.getName() + UNDERSCORE + name; String columnNameInSource = table.getNameInSource() + SOURCE_SEPARATOR + nameInSource(name); addColumn(columnName, columnType, columnValue, true, columnNameInSource, table, mf); } } } else if(isArrayJsonType(element)) { String tableName = table.getName() + UNDERSCORE + dimension.get(); String tableNameInSrc = table.getNameInSource() + SQUARE_BRACKETS; Table subTable = addTable(tableName, tableNameInSrc, true, referenceTableName, dimension, mf); scanRow(keyspace, keyInSource, (JsonValue)element, mf, subTable, referenceTableName, true, dimension); } else { String elementType = getDataType(element); String columnName = table.getName(); String columnNameInSource = table.getNameInSource(); addColumn(columnName, elementType, element, true, columnNameInSource, table, mf); } } } else { String columnName = table.getName(); addColumn(columnName, null, null, true, null, table, mf); } } /** * Principle used to map document-format nested JsonArray to JDBC-compatible Table: * 1) If nested array contains differently-typed elements and no elements are Json document, all elements be * map to same column with Object type. * 2) If nested array contains Json document, all keys be map to Column name, and reference value data type be * map to Column data type * 3) If nested array contains other nested array, or nested array's Json document item contains nested arrays/documents * these nested arrays/documents be treated as Object * 4) A index column used to indicate the index in array. */ private Table addTable(String tableName, String nameInSource, boolean updatable, String referenceTableName, Dimension dimension, MetadataFactory mf) { Table table = null; if (mf.getSchema().getTable(tableName) != null) { table = mf.getSchema().getTable(tableName); } else { table = mf.addTable(tableName); table.setNameInSource(nameInSource); table.setSupportsUpdate(updatable); table.setProperty(IS_ARRAY_TABLE, TRUE_VALUE); Column column = mf.addColumn(DOCUMENTID, STRING, table); column.setUpdatable(true); mf.addForiegnKey("FK0", Arrays.asList(DOCUMENTID), referenceTableName, table); //$NON-NLS-1$ for(int i = 1 ; i <= dimension.dimension ; i ++) { String idxName = buildArrayTableIdxName(nameInSource, i); idxName = repleaceTypedName(referenceTableName, idxName); Column idx = mf.addColumn(idxName, INTEGER, table); idx.setUpdatable(true); } dimension.increment(); } return table; } private void addColumn(String name, String type, Object columnValue, boolean updatable, String nameInSource, Table table, MetadataFactory mf) { String columnName = name; String columnType = type; if(columnType == null && columnValue == null && table.getColumnByName(columnName) == null) { columnType = TypeFacility.RUNTIME_NAMES.STRING; } else if (columnType == null && columnValue == null && table.getColumnByName(columnName) != null) { columnType = table.getColumnByName(columnName).getDatatype().getName(); } if(DataTypeManager.DefaultDataTypes.NULL.equals(columnType)) { columnType = DataTypeManager.DefaultDataTypes.STRING; // how to handle null type? } String tableNameInSource = trimWave(table.getNameInSource()); if(table.getProperty(IS_ARRAY_TABLE, false).equals(FALSE_VALUE) && columnName.startsWith(tableNameInSource)){ columnName = columnName.substring(tableNameInSource.length() + 1); } if (table.getColumnByName(columnName) == null) { Column column = mf.addColumn(columnName, columnType, table); column.setUpdatable(updatable); if(nameInSource != null){ column.setNameInSource(nameInSource); } } else { Column column = table.getColumnByName(columnName); String existColumnType = column.getDatatype().getName(); if(!existColumnType.equals(columnType) && !existColumnType.equals(OBJECT) && columnValue != null) { Datatype datatype = mf.getDataTypes().get(OBJECT); column.setDatatype(datatype, true, 0); } } } private boolean isObjectJsonType(Object jsonValue) { return jsonValue instanceof JsonObject; } private boolean isArrayJsonType(Object jsonValue) { return jsonValue instanceof JsonArray; } /** * For handle typed table name, replace the root keyspace to typed table name. * @param name - typed table name. * @param path - path of document * @return */ private String repleaceTypedName(String name, String path) { String tableName = path.substring(path.indexOf(UNDERSCORE)); return name + tableName; } private String buildArrayTableIdxName(String nameInSource, int dimension) { StringBuilder sb = new StringBuilder(); String dim1Name = nameInSource.substring(0, nameInSource.indexOf(SQUARE_BRACKETS)); String[] names = dim1Name.split(Pattern.quote(SOURCE_SEPARATOR)); boolean isFirst = true; for(String name : names) { if(isFirst) { isFirst = false; sb.append(this.trimWave(name)); } else { sb.append(UNDERSCORE); sb.append(this.trimWave(name)); } } for(int i = 1 ; i <= dimension ; i ++) { if(i == 1) { continue; } sb.append(UNDERSCORE); sb.append(DIM_SUFFIX).append(i); } sb.append(IDX_SUFFIX); return sb.toString(); } private String buildNamedTypePair(String columnIdentifierName, String typedValue) { StringBuilder sb = new StringBuilder(); sb.append(columnIdentifierName).append(COLON).append(QUOTE).append(typedValue).append(QUOTE); return sb.toString(); } private String getTypeName(String keyspace) { if(this.typeNameList == null) { LogManager.logWarning(LogConstants.CTX_CONNECTOR, CouchbasePlugin.Util.gs(CouchbasePlugin.Event.TEIID29009)); return null; } if(this.typeNameMap == null) { this.typeNameMap = new HashMap<>(); try { Pattern typeNamePattern = Pattern.compile(CouchbaseProperties.TPYENAME_MATCHER_PATTERN); Matcher typeGroupMatch = typeNamePattern.matcher(typeNameList); while (typeGroupMatch.find()) { typeNameMap.put(typeGroupMatch.group(1), typeGroupMatch.group(2)); } } catch (Exception e) { LogManager.logError(LogConstants.CTX_CONNECTOR, e, CouchbasePlugin.Util.gs(CouchbasePlugin.Event.TEIID29012, typeNameList)); } } return this.typeNameMap.get(keyspace); } protected void addProcedures(MetadataFactory metadataFactory, CouchbaseConnection connection) { Procedure getDocuments = metadataFactory.addProcedure(GETDOCUMENTS); getDocuments.setAnnotation(CouchbasePlugin.Util.getString("getDocuments.Annotation")); //$NON-NLS-1$ ProcedureParameter param = metadataFactory.addProcedureParameter(ID, TypeFacility.RUNTIME_NAMES.STRING, Type.In, getDocuments); param.setNullType(No_Nulls); param.setAnnotation(CouchbasePlugin.Util.getString("getDocuments.id.Annotation")); //$NON-NLS-1$ param = metadataFactory.addProcedureParameter(KEYSPACE, TypeFacility.RUNTIME_NAMES.STRING, Type.In, getDocuments); param.setNullType(No_Nulls); param.setAnnotation(CouchbasePlugin.Util.getString("getDocuments.keyspace.Annotation")); //$NON-NLS-1$ metadataFactory.addProcedureResultSetColumn(RESULT, TypeFacility.RUNTIME_NAMES.BLOB, getDocuments); Procedure getDocument = metadataFactory.addProcedure(GETDOCUMENT); getDocument.setAnnotation(CouchbasePlugin.Util.getString("getDocument.Annotation")); //$NON-NLS-1$ param = metadataFactory.addProcedureParameter(ID, TypeFacility.RUNTIME_NAMES.STRING, Type.In, getDocument); param.setNullType(No_Nulls); param.setAnnotation(CouchbasePlugin.Util.getString("getDocument.id.Annotation")); //$NON-NLS-1$ param = metadataFactory.addProcedureParameter(KEYSPACE, TypeFacility.RUNTIME_NAMES.STRING, Type.In, getDocument); param.setNullType(No_Nulls); param.setAnnotation(CouchbasePlugin.Util.getString("getDocument.keyspace.Annotation")); //$NON-NLS-1$ metadataFactory.addProcedureResultSetColumn(RESULT, TypeFacility.RUNTIME_NAMES.BLOB, getDocument); } /** * All supported type in a Couchbase JSON item: * null, String, Integer, Long, Double, Boolean, * BigInteger, BigDecimal, JsonObject, JsonArray * @param value * @return */ private String getDataType(Object value) { if(value == null) { return TypeFacility.RUNTIME_NAMES.NULL; } else if (value instanceof String) { return TypeFacility.RUNTIME_NAMES.STRING; } else if (value instanceof Integer) { return TypeFacility.RUNTIME_NAMES.INTEGER; } else if (value instanceof Long) { return TypeFacility.RUNTIME_NAMES.LONG; } else if (value instanceof Double) { return TypeFacility.RUNTIME_NAMES.DOUBLE; } else if (value instanceof Boolean) { return TypeFacility.RUNTIME_NAMES.BOOLEAN; } else if (value instanceof BigInteger) { return TypeFacility.RUNTIME_NAMES.BIG_INTEGER; } else if (value instanceof BigDecimal) { return TypeFacility.RUNTIME_NAMES.BIG_DECIMAL; } return TypeFacility.RUNTIME_NAMES.OBJECT; } private String buildN1QLNamespaces() { return "SELECT name FROM system:namespaces"; //$NON-NLS-1$ } private String buildN1QLKeyspaces(String namespace) { return "SELECT name, namespace_id FROM system:keyspaces WHERE namespace_id = '" + namespace + "'"; //$NON-NLS-1$ //$NON-NLS-2$ } private String buildN1QLTypeQuery(String typeName, String namespace, String keyspace) { StringBuilder sb = new StringBuilder(); sb.append("SELECT DISTINCT "); //$NON-NLS-1$ sb.append(typeName); sb.append(buildN1QLFrom(namespace, keyspace)); return sb.toString(); } private String buildN1QLQuery(String columnIdentifierName, String typedValue, String namespace, String keyspace, int sampleSize, boolean hasTypeIdentifier) { StringBuilder sb = new StringBuilder(); sb.append("SELECT meta(").append(WAVE); //$NON-NLS-1$ sb.append(keyspace); sb.append(WAVE).append(").id as PK, "); //$NON-NLS-1$ sb.append(WAVE).append(keyspace).append(WAVE); sb.append(buildN1QLFrom(namespace, keyspace)); if(hasTypeIdentifier) { sb.append(" WHERE ").append(columnIdentifierName).append("='").append(typedValue).append("'"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } sb.append(" LIMIT ").append(sampleSize); //$NON-NLS-1$ return sb.toString(); } private String buildN1QLFrom(String namespace, String keyspace) { StringBuilder sb = new StringBuilder(); sb.append(" FROM "); //$NON-NLS-1$ sb.append(WAVE).append(namespace).append(WAVE); sb.append(COLON); sb.append(WAVE).append(keyspace).append(WAVE); return sb.toString(); } private String trimWave(String value) { String results = value; if(results.startsWith(WAVE)) { results = results.substring(1); } if(results.endsWith(WAVE)) { results = results.substring(0, results.length() - 1); } return results; } private String nameInSource(String path) { return WAVE + path + WAVE; } @TranslatorProperty(display = "SampleSize", category = PropertyType.IMPORT, description = "Maximum number of documents per keyspace that should be map") //$NON-NLS-1$ //$NON-NLS-2$ public int getSampleSize() { return sampleSize; } public void setSampleSize(int sampleSize) { this.sampleSize = sampleSize; } @TranslatorProperty(display = "TypeNameList", category = PropertyType.IMPORT, description = "A comma-separate list of the attributes that the buckets use to specify document types. Each list item must be a bucket name surrounded by back quotes (`), a colon (:), and an attribute name surrounded by back quotes (`).") //$NON-NLS-1$ //$NON-NLS-2$ public String getTypeNameList() { return typeNameList; } public void setTypeNameList(String typeNameList) { this.typeNameList = typeNameList; } /** * The dimension of nested array, a dimension is a hint of nested array table name, and index name. * * For example, the following JsonObject is a 3 dimension nested array, * <pre> {@code * { * "default": {"nested": [[["dimension 3"]]]} * } * }</pre> * each dimension reference with a table, total 3 tables will be generated: * default_nested * default_nested_dim2 * default_nested_dim2_dim3 * * The nested array contains it's index of it's parent array, the return of query * 'SELECT * FROM default_nested_dim2_dim3' looks * <pre> {@code * +-------------+--------------------+-------------------------+------------------------------+--------------------------+ * | ID | default_nested_idx | default_nested_dim2_idx | default_nested_dim2_dim3_idx | default_nested_dim2_dim3 | * +-------------+--------------------+-------------------------+------------------------------+--------------------------+ * | nestedArray | 0 | 0 | 0 | dimension 3 | * +-------------+--------------------+-------------------------+------------------------------+--------------------------+ *}</pre> */ public static class Dimension implements Comparable<Dimension> { private final String name; int dimension; public Dimension() { this.name = DIM_SUFFIX ; this.dimension = 1; } public void increment() { dimension++; } public String get(){ return this.name + this.dimension; } public int dim() { return this.dimension; } @Override public int compareTo(Dimension dim) { if(this.dimension < dim.dimension) { return -1; } else if(this.dimension > dim.dimension) { return 1; } else { return 0; } } } }