diff --git a/assemblies/debug/pom.xml b/assemblies/debug/pom.xml index f734be09fa1..98843387ddb 100644 --- a/assemblies/debug/pom.xml +++ b/assemblies/debug/pom.xml @@ -367,6 +367,12 @@ ${project.version} provided + + org.apache.hop + hop-misc-naming + ${project.version} + provided + org.apache.hop hop-misc-projects diff --git a/assemblies/plugins/pom.xml b/assemblies/plugins/pom.xml index 5198265e75f..abc48d82af6 100644 --- a/assemblies/plugins/pom.xml +++ b/assemblies/plugins/pom.xml @@ -652,6 +652,12 @@ ${project.version} zip + + org.apache.hop + hop-misc-naming + ${project.version} + zip + org.apache.hop hop-misc-passwords diff --git a/core/src/main/java/org/apache/hop/metadata/api/HopMetadataPropertyType.java b/core/src/main/java/org/apache/hop/metadata/api/HopMetadataPropertyType.java index e9936a0e6cc..7b84c8561b5 100644 --- a/core/src/main/java/org/apache/hop/metadata/api/HopMetadataPropertyType.java +++ b/core/src/main/java/org/apache/hop/metadata/api/HopMetadataPropertyType.java @@ -95,6 +95,9 @@ public enum HopMetadataPropertyType { // STATIC SCHEMA STATIC_SCHEMA_DEFINITION, + // NAMING + NAMING_SCHEME, + // VFS VFS_GCP_CONNECTION, VFS_AZURE_CONNECTION, diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/metadata-types/naming-scheme-csv-input-after.png b/docs/hop-user-manual/modules/ROOT/assets/images/metadata-types/naming-scheme-csv-input-after.png new file mode 100644 index 00000000000..a5fbdbc0cc5 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/metadata-types/naming-scheme-csv-input-after.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/metadata-types/naming-scheme-csv-input-before.png b/docs/hop-user-manual/modules/ROOT/assets/images/metadata-types/naming-scheme-csv-input-before.png new file mode 100644 index 00000000000..e7013eb85d7 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/metadata-types/naming-scheme-csv-input-before.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/metadata-types/naming-scheme-editor.png b/docs/hop-user-manual/modules/ROOT/assets/images/metadata-types/naming-scheme-editor.png new file mode 100644 index 00000000000..54f90eedb2b Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/metadata-types/naming-scheme-editor.png differ diff --git a/docs/hop-user-manual/modules/ROOT/nav.adoc b/docs/hop-user-manual/modules/ROOT/nav.adoc index dd0909eb259..3978c3123fe 100644 --- a/docs/hop-user-manual/modules/ROOT/nav.adoc +++ b/docs/hop-user-manual/modules/ROOT/nav.adoc @@ -488,6 +488,7 @@ under the License. *** xref:metadata-types/variable-resolver/openbao-variable-resolver.adoc[OpenBAO variable resolver] *** xref:metadata-types/variable-resolver/pipeline-variable-resolver.adoc[Pipeline variable resolver] ** xref:metadata-types/static-schema-definition.adoc[Static Schema Definition] +** xref:metadata-types/naming-scheme.adoc[Naming Scheme] ** xref:hop-server/web-service.adoc[Web Service] ** xref:metadata-types/webdav-connection.adoc[WebDAV Connection] ** xref:metadata-types/workflow-log.adoc[Workflow Log] diff --git a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/index.adoc b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/index.adoc index c4c716ac2d9..e3ee0c4a487 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/index.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/index.adoc @@ -49,6 +49,7 @@ xref:metadata-types/data-stream/data-stream.adoc[Data Stream]: A Data Stream can * xref:metadata-types/mongodb-connection.adoc[MongoDB Connection]: Describes a MongoDB connection * xref:metadata-types/mail-server-connection.adoc[Mail Server Connection]: Describes a mail server connection * xref:metadata-types/minio-connection.adoc[Minio (S3) Connection]: Connect to one or more S3 endpoints using a Minio client +* xref:metadata-types/naming-scheme.adoc[Naming Scheme]: Reusable rules to normalize Hop field names, database identifiers, and file or folder names * xref:metadata-types/webdav-connection.adoc[WebDAV Connection]: Named WebDAV or WebDAV-over-HTTPS endpoint used as a VFS scheme in file paths * xref:metadata-types/neo4j/neo4j-connection.adoc[Neo4j Connection]: A shared connection to a Neo4j server * xref:metadata-types/neo4j/neo4j-graphmodel.adoc[Neo4j Graph Model]: Description of the nodes, relationships, indexes, ... of a Neo4j graph diff --git a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/naming-scheme.adoc b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/naming-scheme.adoc new file mode 100644 index 00000000000..b29a526a48a --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/naming-scheme.adoc @@ -0,0 +1,92 @@ +//// +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. +//// +:imagesdir: ../../assets/images/ +:page-pagination: +:description: A Naming Scheme defines reusable rules to normalize Hop field names, database identifiers, and file or folder names. + += Naming Scheme + +== Description + +A *Naming Scheme* describes how free-form names should be turned into consistent identifiers. +Typical uses include cleaning field names that arrive with spaces (for example `Order ID` → `order_id`) and applying a shared project convention (snake_case, camelCase, PascalCase, and so on). + +Naming Schemes are managed as project metadata. You can apply a scheme of type *Hop field names* from any editable table in Hop Gui via the TableView toolbar action **Apply a Naming Scheme to a column**. + +NOTE: Types for database tables/columns and files/folders are reserved for future UI surfaces; the same engine and options apply. + +== Options + +image:metadata-types/naming-scheme-editor.png[Naming Scheme editor, width="75%"] + +The screenshot above shows a scheme named `lowercase-dashes`: lower case words joined with dashes, so `Order ID` previews as `order-id`. + +[options="header"] +|=== +|Option|Description +|Name|Unique metadata name of this scheme. +|Description|Optional free-text description. +|Type|Target kind: Hop field names, database tables, database columns, files, or folders. +|Case style|`As is`, `Lower case`, `Upper case`, `camelCase`, or `PascalCase`. +|Word separator|How words are joined when not using camel/Pascal case: none, underscore (`_`), or dash (`-`). +|Extra delimiters|Additional characters treated as word boundaries (beyond spaces, `_`, `-`, and camelCase edges). +|Remove special characters|When enabled, strip non letter/digit characters from each word. +|Collapse repeated separators|When enabled, collapse repeated separators such as `__` to a single separator. +|Trim leading/trailing separators|When enabled, remove separators at the start or end of the result. +|Prefix / Suffix|Optional strings prepended or appended after the transform. +|Preview input / output|Live sample showing how the current options transform a name. +|=== + +== Examples + +[options="header"] +|=== +|Input|Case style|Word separator|Result +|`Order ID`|Lower case|Underscore (`_`)|`order_id` +|`Order ID`|As is|Underscore (`_`)|`Order_ID` +|`Order ID`|camelCase|_|`orderId` +|`Order ID`|PascalCase|_|`OrderId` +|`order-id`|Upper case|Underscore (`_`)|`ORDER_ID` +|`Order ID`|Lower case|Dash (`-`)|`order-id` +|=== + +== Applying from TableView + +. Open a transform or dialog that shows a field list (or any other editable table). +. Ensure table toolbars are visible (Hop Gui options). +. Click the **Apply a Naming Scheme to a column** toolbar icon (label / tag icon on the TableView toolbar). +. Select the column to rename (usually the first / name column). +. Select a Naming Scheme of type *Hop field names*. +. Confirm — values in non-empty rows are updated. Use the table Undo action if needed. + +TIP: Create at least one Naming Scheme in the Metadata perspective before using the toolbar action. + +=== Example: CSV File Input fields + +The following example uses the `lowercase-dashes` scheme on the **Name** column of a xref:pipeline/transforms/csvinput.adoc[CSV File Input] field list. + +*Before* — mixed styles (`ID`, `FirstName`, `birth_date`, `stateCode`, …): + +image:metadata-types/naming-scheme-csv-input-before.png[CSV File Input fields before applying a Naming Scheme, width="90%"] + +*After* — consistent lower-case names with dashes (`id`, `first-name`, `birth-date`, `state-code`, …): + +image:metadata-types/naming-scheme-csv-input-after.png[CSV File Input fields after applying the lowercase-dashes Naming Scheme, width="90%"] + +== Related + +* GitHub issue https://github.com/apache/hop/issues/2683[#2683] — field name cleanup / naming toolbar diff --git a/plugins/misc/naming/pom.xml b/plugins/misc/naming/pom.xml new file mode 100644 index 00000000000..fac4155ef17 --- /dev/null +++ b/plugins/misc/naming/pom.xml @@ -0,0 +1,32 @@ + + + + 4.0.0 + + + org.apache.hop + hop-plugins-misc + 2.19.0-SNAPSHOT + + + hop-misc-naming + jar + Hop Naming Scheme + + diff --git a/plugins/misc/naming/src/assembly/assembly.xml b/plugins/misc/naming/src/assembly/assembly.xml new file mode 100644 index 00000000000..3bb294e56e6 --- /dev/null +++ b/plugins/misc/naming/src/assembly/assembly.xml @@ -0,0 +1,35 @@ + + + + hop-misc-naming + + zip + + . + + + + org.apache.hop:hop-misc-naming:jar + + lib/core + + + diff --git a/plugins/misc/naming/src/main/java/org/apache/hop/naming/engine/NamingEngine.java b/plugins/misc/naming/src/main/java/org/apache/hop/naming/engine/NamingEngine.java new file mode 100644 index 00000000000..0cf9c2179f6 --- /dev/null +++ b/plugins/misc/naming/src/main/java/org/apache/hop/naming/engine/NamingEngine.java @@ -0,0 +1,218 @@ +/* + * 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.hop.naming.engine; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.naming.metadata.NamingCaseStyle; +import org.apache.hop.naming.metadata.NamingScheme; +import org.apache.hop.naming.metadata.NamingWordSeparator; + +/** + * Pure string transform that applies a {@link NamingScheme} to an input name. No UI or metadata + * provider dependencies — safe to unit-test and reuse from transforms later. + */ +public final class NamingEngine { + + private NamingEngine() { + // utility + } + + /** + * Apply the scheme to {@code input}. Null input returns null; empty input returns empty. + * + * @param scheme naming rules (null is treated as default hop-field snake_case-ish scheme) + * @param input original name + * @return transformed name + */ + public static String apply(NamingScheme scheme, String input) { + if (input == null) { + return null; + } + if (input.isEmpty()) { + return input; + } + + NamingScheme effective = scheme != null ? scheme : new NamingScheme(); + NamingCaseStyle caseStyle = NamingCaseStyle.fromCode(effective.getCaseStyle()); + NamingWordSeparator wordSeparator = NamingWordSeparator.fromCode(effective.getWordSeparator()); + + List words = splitWords(input, effective.getExtraDelimiters()); + if (effective.isRemoveSpecialCharacters()) { + words = removeSpecialCharacters(words); + } + if (words.isEmpty()) { + return applyAffixes(effective, ""); + } + + String joined = joinWords(words, caseStyle, wordSeparator); + joined = postProcessSeparators(joined, wordSeparator, effective); + return applyAffixes(effective, joined); + } + + static List splitWords(String input, String extraDelimiters) { + String prepared = insertCamelCaseBoundaries(input.trim()); + StringBuilder delimiters = new StringBuilder(" \t\n\r_-"); + if (StringUtils.isNotEmpty(extraDelimiters)) { + delimiters.append(extraDelimiters); + } + + List words = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + for (int i = 0; i < prepared.length(); i++) { + char c = prepared.charAt(i); + if (delimiters.indexOf(String.valueOf(c)) >= 0) { + if (!current.isEmpty()) { + words.add(current.toString()); + current.setLength(0); + } + } else { + current.append(c); + } + } + if (!current.isEmpty()) { + words.add(current.toString()); + } + return words; + } + + /** + * Insert spaces at camelCase and letter/digit boundaries so later delimiter splitting sees + * separate words. Acronyms: {@code XMLParser} → {@code XML Parser}. + */ + static String insertCamelCaseBoundaries(String input) { + if (input.length() < 2) { + return input; + } + StringBuilder sb = new StringBuilder(input.length() + 8); + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + if (i > 0) { + char prev = input.charAt(i - 1); + char next = i + 1 < input.length() ? input.charAt(i + 1) : 0; + boolean lowerThenUpper = Character.isLowerCase(prev) && Character.isUpperCase(c); + boolean acronymBoundary = + Character.isUpperCase(prev) + && Character.isUpperCase(c) + && next != 0 + && Character.isLowerCase(next); + boolean letterDigit = + (Character.isLetter(prev) && Character.isDigit(c)) + || (Character.isDigit(prev) && Character.isLetter(c)); + if (lowerThenUpper || acronymBoundary || letterDigit) { + sb.append(' '); + } + } + sb.append(c); + } + return sb.toString(); + } + + private static List removeSpecialCharacters(List words) { + List cleaned = new ArrayList<>(); + for (String word : words) { + StringBuilder sb = new StringBuilder(word.length()); + for (int i = 0; i < word.length(); i++) { + char c = word.charAt(i); + if (Character.isLetterOrDigit(c)) { + sb.append(c); + } + } + if (!sb.isEmpty()) { + cleaned.add(sb.toString()); + } + } + return cleaned; + } + + private static String joinWords( + List words, NamingCaseStyle caseStyle, NamingWordSeparator wordSeparator) { + if (caseStyle == NamingCaseStyle.CAMEL || caseStyle == NamingCaseStyle.PASCAL) { + return joinCamel(words, caseStyle == NamingCaseStyle.PASCAL); + } + + String sep = wordSeparator.getValue(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < words.size(); i++) { + if (i > 0 && !sep.isEmpty()) { + sb.append(sep); + } + sb.append(applyCaseToWord(words.get(i), caseStyle)); + } + return sb.toString(); + } + + private static String joinCamel(List words, boolean pascal) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < words.size(); i++) { + String word = words.get(i); + if (word.isEmpty()) { + continue; + } + String lower = word.toLowerCase(Locale.ROOT); + if (i == 0 && !pascal) { + sb.append(lower); + } else { + sb.append(Character.toUpperCase(lower.charAt(0))); + if (lower.length() > 1) { + sb.append(lower.substring(1)); + } + } + } + return sb.toString(); + } + + private static String applyCaseToWord(String word, NamingCaseStyle caseStyle) { + return switch (caseStyle) { + case LOWER -> word.toLowerCase(Locale.ROOT); + case UPPER -> word.toUpperCase(Locale.ROOT); + default -> word; // AS_IS + }; + } + + private static String postProcessSeparators( + String joined, NamingWordSeparator wordSeparator, NamingScheme scheme) { + String result = joined; + String sep = wordSeparator.getValue(); + if (StringUtils.isNotEmpty(sep)) { + if (scheme.isCollapseRepeatedSeparators()) { + String doubled = sep + sep; + while (result.contains(doubled)) { + result = result.replace(doubled, sep); + } + } + if (scheme.isTrimEdgeSeparators()) { + while (result.startsWith(sep)) { + result = result.substring(sep.length()); + } + while (result.endsWith(sep)) { + result = result.substring(0, result.length() - sep.length()); + } + } + } + return result; + } + + private static String applyAffixes(NamingScheme scheme, String value) { + String prefix = StringUtils.defaultString(scheme.getPrefix()); + String suffix = StringUtils.defaultString(scheme.getSuffix()); + return prefix + value + suffix; + } +} diff --git a/plugins/misc/naming/src/main/java/org/apache/hop/naming/gui/TableViewNamingToolbarButton.java b/plugins/misc/naming/src/main/java/org/apache/hop/naming/gui/TableViewNamingToolbarButton.java new file mode 100644 index 00000000000..93b28292961 --- /dev/null +++ b/plugins/misc/naming/src/main/java/org/apache/hop/naming/gui/TableViewNamingToolbarButton.java @@ -0,0 +1,161 @@ +/* + * 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.hop.naming.gui; + +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.gui.plugin.GuiPlugin; +import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElement; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.metadata.api.IHopMetadataSerializer; +import org.apache.hop.naming.engine.NamingEngine; +import org.apache.hop.naming.metadata.NamingScheme; +import org.apache.hop.naming.metadata.NamingSchemeType; +import org.apache.hop.ui.core.dialog.EnterSelectionDialog; +import org.apache.hop.ui.core.dialog.ErrorDialog; +import org.apache.hop.ui.core.dialog.MessageBox; +import org.apache.hop.ui.core.widget.ColumnInfo; +import org.apache.hop.ui.core.widget.TableView; +import org.apache.hop.ui.hopgui.HopGui; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.TableItem; + +/** + * TableView toolbar action that applies a {@link NamingScheme} of type Hop field names to a chosen + * column (issue #2683). + */ +@GuiPlugin +public class TableViewNamingToolbarButton { + + private static final Class PKG = TableViewNamingToolbarButton.class; + + public static final String ID_TOOLBAR_APPLY_NAMING = + "tableview-toolbar-30100-apply-naming-scheme"; + + @GuiToolbarElement( + root = TableView.ID_TOOLBAR, + id = ID_TOOLBAR_APPLY_NAMING, + toolTip = "i18n::Naming.Toolbar.Apply.ToolTip", + separator = true, + image = "naming.svg") + public static void applyNamingScheme(TableView tableView) { + if (tableView == null || tableView.isDisposed() || tableView.isReadonly()) { + return; + } + + Shell shell = tableView.getShell(); + ColumnInfo[] columns = tableView.getColumns(); + if (columns == null || columns.length == 0) { + return; + } + + try { + // 1. Pick column (default: first data column) + // + String[] columnNames = new String[columns.length]; + for (int i = 0; i < columns.length; i++) { + columnNames[i] = columns[i].getName(); + } + EnterSelectionDialog columnDialog = + new EnterSelectionDialog( + shell, + columnNames, + BaseMessages.getString(PKG, "Naming.ColumnSelection.Title"), + BaseMessages.getString(PKG, "Naming.ColumnSelection.Message")); + columnDialog.setSelectedNrs(new int[] {0}); + if (columnDialog.open() == null) { + return; + } + int columnIndex = columnDialog.getSelectionNr(); + if (columnIndex < 0 || columnIndex >= columns.length) { + return; + } + // TableItem column 0 is "#"; data columns start at 1 + int tableColNr = columnIndex + 1; + + // 2. Pick Naming Scheme (Hop field names) + // + IHopMetadataSerializer serializer = + HopGui.getInstance().getMetadataProvider().getSerializer(NamingScheme.class); + List allSchemes = serializer.loadAll(); + List fieldSchemes = new ArrayList<>(); + for (NamingScheme scheme : allSchemes) { + NamingSchemeType type = NamingSchemeType.fromCode(scheme.getType()); + if (type == NamingSchemeType.HOP_FIELD) { + fieldSchemes.add(scheme); + } + } + if (fieldSchemes.isEmpty()) { + MessageBox box = new MessageBox(shell, SWT.ICON_INFORMATION | SWT.OK); + box.setText(BaseMessages.getString(PKG, "Naming.NoSchemes.Title")); + box.setMessage(BaseMessages.getString(PKG, "Naming.NoSchemes.Message")); + box.open(); + return; + } + + String[] schemeNames = new String[fieldSchemes.size()]; + for (int i = 0; i < fieldSchemes.size(); i++) { + schemeNames[i] = fieldSchemes.get(i).getName(); + } + EnterSelectionDialog schemeDialog = + new EnterSelectionDialog( + shell, + schemeNames, + BaseMessages.getString(PKG, "Naming.SchemeSelection.Title"), + BaseMessages.getString(PKG, "Naming.SchemeSelection.Message")); + if (schemeDialog.open() == null) { + return; + } + int schemeIndex = schemeDialog.getSelectionNr(); + if (schemeIndex < 0 || schemeIndex >= fieldSchemes.size()) { + return; + } + NamingScheme scheme = fieldSchemes.get(schemeIndex); + + // 3. Apply to non-empty rows + // + List items = tableView.getNonEmptyItems(); + if (items.isEmpty()) { + return; + } + + int[] rowIndices = new int[items.size()]; + String[] newValues = new String[items.size()]; + for (int i = 0; i < items.size(); i++) { + TableItem item = items.get(i); + rowIndices[i] = tableView.getTable().indexOf(item); + String current = item.getText(tableColNr); + if (StringUtils.isEmpty(current) || "".equals(current)) { + newValues[i] = current; + } else { + newValues[i] = NamingEngine.apply(scheme, current); + } + } + + tableView.applyColumnValues(tableColNr, rowIndices, newValues); + } catch (Exception e) { + new ErrorDialog( + shell, + BaseMessages.getString(PKG, "Naming.Error.Title"), + BaseMessages.getString(PKG, "Naming.Error.Message"), + e); + } + } +} diff --git a/plugins/misc/naming/src/main/java/org/apache/hop/naming/metadata/NamingCaseStyle.java b/plugins/misc/naming/src/main/java/org/apache/hop/naming/metadata/NamingCaseStyle.java new file mode 100644 index 00000000000..0a6f3e72722 --- /dev/null +++ b/plugins/misc/naming/src/main/java/org/apache/hop/naming/metadata/NamingCaseStyle.java @@ -0,0 +1,81 @@ +/* + * 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.hop.naming.metadata; + +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.i18n.BaseMessages; + +/** Case transformation applied when joining name words. */ +public enum NamingCaseStyle { + AS_IS("as-is", "NamingCaseStyle.AsIs"), + LOWER("lower", "NamingCaseStyle.Lower"), + UPPER("upper", "NamingCaseStyle.Upper"), + CAMEL("camel", "NamingCaseStyle.Camel"), + PASCAL("pascal", "NamingCaseStyle.Pascal"); + + private static final Class PKG = NamingScheme.class; + + private final String code; + private final String i18nKey; + + NamingCaseStyle(String code, String i18nKey) { + this.code = code; + this.i18nKey = i18nKey; + } + + public String getCode() { + return code; + } + + public String getLabel() { + return BaseMessages.getString(PKG, i18nKey); + } + + public static NamingCaseStyle fromCode(String code) { + if (StringUtils.isEmpty(code)) { + return LOWER; + } + for (NamingCaseStyle style : values()) { + if (style.code.equalsIgnoreCase(code.trim())) { + return style; + } + } + return LOWER; + } + + public static String[] getLabels() { + NamingCaseStyle[] values = values(); + String[] labels = new String[values.length]; + for (int i = 0; i < values.length; i++) { + labels[i] = values[i].getLabel(); + } + return labels; + } + + public static NamingCaseStyle fromLabel(String label) { + if (StringUtils.isEmpty(label)) { + return LOWER; + } + for (NamingCaseStyle style : values()) { + if (style.getLabel().equals(label) || style.code.equalsIgnoreCase(label.trim())) { + return style; + } + } + return LOWER; + } +} diff --git a/plugins/misc/naming/src/main/java/org/apache/hop/naming/metadata/NamingScheme.java b/plugins/misc/naming/src/main/java/org/apache/hop/naming/metadata/NamingScheme.java new file mode 100644 index 00000000000..a13e80bc5fc --- /dev/null +++ b/plugins/misc/naming/src/main/java/org/apache/hop/naming/metadata/NamingScheme.java @@ -0,0 +1,93 @@ +/* + * 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.hop.naming.metadata; + +import java.io.Serializable; +import lombok.Getter; +import lombok.Setter; +import org.apache.hop.metadata.api.HopMetadata; +import org.apache.hop.metadata.api.HopMetadataBase; +import org.apache.hop.metadata.api.HopMetadataCategory; +import org.apache.hop.metadata.api.HopMetadataProperty; +import org.apache.hop.metadata.api.HopMetadataPropertyType; +import org.apache.hop.metadata.api.IHopMetadata; + +/** + * Reusable naming rules for identifiers such as Hop field names, database tables/columns, and + * file/folder names. Apply via the TableView toolbar (Hop field names) or programmatically with + * {@link org.apache.hop.naming.engine.NamingEngine}. + */ +@Getter +@Setter +@HopMetadata( + key = "naming-scheme", + name = "i18n::NamingScheme.Name", + description = "i18n::NamingScheme.Description", + image = "naming.svg", + category = HopMetadataCategory.DATA_DEFINITION, + documentationUrl = "/metadata-types/naming-scheme.html", + hopMetadataPropertyType = HopMetadataPropertyType.NAMING_SCHEME) +public class NamingScheme extends HopMetadataBase implements Serializable, IHopMetadata { + + @HopMetadataProperty private String description; + + /** Target kind code: {@link NamingSchemeType} (default {@code hop-field}). */ + @HopMetadataProperty private String type; + + /** Case style code: {@link NamingCaseStyle}. */ + @HopMetadataProperty private String caseStyle; + + /** Word separator code: {@link NamingWordSeparator}. */ + @HopMetadataProperty private String wordSeparator; + + /** + * Extra characters treated as word boundaries in addition to whitespace, underscore, dash and + * camelCase edges (for example {@code .#}). + */ + @HopMetadataProperty private String extraDelimiters; + + /** Strip characters that are not letters or digits from each word. */ + @HopMetadataProperty private boolean removeSpecialCharacters; + + /** Collapse repeated word separators (for example {@code __} → {@code _}). */ + @HopMetadataProperty private boolean collapseRepeatedSeparators; + + /** Remove leading/trailing word separators from the result. */ + @HopMetadataProperty private boolean trimEdgeSeparators; + + @HopMetadataProperty private String prefix; + + @HopMetadataProperty private String suffix; + + public NamingScheme() { + this.type = NamingSchemeType.HOP_FIELD.getCode(); + this.caseStyle = NamingCaseStyle.LOWER.getCode(); + this.wordSeparator = NamingWordSeparator.UNDERSCORE.getCode(); + this.extraDelimiters = ""; + this.removeSpecialCharacters = true; + this.collapseRepeatedSeparators = true; + this.trimEdgeSeparators = true; + this.prefix = ""; + this.suffix = ""; + } + + public NamingScheme(String name) { + this(); + this.name = name; + } +} diff --git a/plugins/misc/naming/src/main/java/org/apache/hop/naming/metadata/NamingSchemeEditor.java b/plugins/misc/naming/src/main/java/org/apache/hop/naming/metadata/NamingSchemeEditor.java new file mode 100644 index 00000000000..86b95b4af6d --- /dev/null +++ b/plugins/misc/naming/src/main/java/org/apache/hop/naming/metadata/NamingSchemeEditor.java @@ -0,0 +1,408 @@ +/* + * 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.hop.naming.metadata; + +import org.apache.hop.core.Const; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.naming.engine.NamingEngine; +import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.metadata.MetadataEditor; +import org.apache.hop.ui.core.metadata.MetadataManager; +import org.apache.hop.ui.hopgui.HopGui; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +public class NamingSchemeEditor extends MetadataEditor { + + private static final Class PKG = NamingScheme.class; + + private Text wName; + private Text wDescription; + private Combo wType; + private Combo wCaseStyle; + private Combo wWordSeparator; + private Text wExtraDelimiters; + private Button wRemoveSpecial; + private Button wCollapseSeparators; + private Button wTrimEdges; + private Text wPrefix; + private Text wSuffix; + private Text wPreviewInput; + private Text wPreviewOutput; + + public NamingSchemeEditor( + HopGui hopGui, MetadataManager manager, NamingScheme metadata) { + super(hopGui, manager, metadata); + } + + @Override + public void createControl(Composite parent) { + PropsUi props = PropsUi.getInstance(); + int middle = props.getMiddlePct(); + int margin = PropsUi.getMargin() + 2; + + // Name + Label wlName = new Label(parent, SWT.RIGHT); + PropsUi.setLook(wlName); + wlName.setText(BaseMessages.getString(PKG, "NamingSchemeEditor.Name.Label")); + FormData fdlName = new FormData(); + fdlName.top = new FormAttachment(0, margin); + fdlName.left = new FormAttachment(0, 0); + fdlName.right = new FormAttachment(middle, -margin); + wlName.setLayoutData(fdlName); + wName = new Text(parent, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wName); + FormData fdName = new FormData(); + fdName.top = new FormAttachment(wlName, 0, SWT.CENTER); + fdName.left = new FormAttachment(middle, 0); + fdName.right = new FormAttachment(100, 0); + wName.setLayoutData(fdName); + Control lastControl = wName; + + // Description + Label wlDescription = new Label(parent, SWT.RIGHT); + PropsUi.setLook(wlDescription); + wlDescription.setText(BaseMessages.getString(PKG, "NamingSchemeEditor.Description.Label")); + FormData fdlDescription = new FormData(); + fdlDescription.top = new FormAttachment(lastControl, margin); + fdlDescription.left = new FormAttachment(0, 0); + fdlDescription.right = new FormAttachment(middle, -margin); + wlDescription.setLayoutData(fdlDescription); + wDescription = new Text(parent, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wDescription); + FormData fdDescription = new FormData(); + fdDescription.top = new FormAttachment(wlDescription, 0, SWT.CENTER); + fdDescription.left = new FormAttachment(middle, 0); + fdDescription.right = new FormAttachment(100, 0); + wDescription.setLayoutData(fdDescription); + lastControl = wDescription; + + // Type + Label wlType = new Label(parent, SWT.RIGHT); + PropsUi.setLook(wlType); + wlType.setText(BaseMessages.getString(PKG, "NamingSchemeEditor.Type.Label")); + FormData fdlType = new FormData(); + fdlType.top = new FormAttachment(lastControl, margin); + fdlType.left = new FormAttachment(0, 0); + fdlType.right = new FormAttachment(middle, -margin); + wlType.setLayoutData(fdlType); + wType = new Combo(parent, SWT.SINGLE | SWT.READ_ONLY | SWT.BORDER | SWT.LEFT); + PropsUi.setLook(wType); + wType.setItems(NamingSchemeType.getLabels()); + FormData fdType = new FormData(); + fdType.top = new FormAttachment(wlType, 0, SWT.CENTER); + fdType.left = new FormAttachment(middle, 0); + fdType.right = new FormAttachment(100, 0); + wType.setLayoutData(fdType); + lastControl = wType; + + // Case style + Label wlCaseStyle = new Label(parent, SWT.RIGHT); + PropsUi.setLook(wlCaseStyle); + wlCaseStyle.setText(BaseMessages.getString(PKG, "NamingSchemeEditor.CaseStyle.Label")); + FormData fdlCaseStyle = new FormData(); + fdlCaseStyle.top = new FormAttachment(lastControl, margin); + fdlCaseStyle.left = new FormAttachment(0, 0); + fdlCaseStyle.right = new FormAttachment(middle, -margin); + wlCaseStyle.setLayoutData(fdlCaseStyle); + wCaseStyle = new Combo(parent, SWT.SINGLE | SWT.READ_ONLY | SWT.BORDER | SWT.LEFT); + PropsUi.setLook(wCaseStyle); + wCaseStyle.setItems(NamingCaseStyle.getLabels()); + FormData fdCaseStyle = new FormData(); + fdCaseStyle.top = new FormAttachment(wlCaseStyle, 0, SWT.CENTER); + fdCaseStyle.left = new FormAttachment(middle, 0); + fdCaseStyle.right = new FormAttachment(100, 0); + wCaseStyle.setLayoutData(fdCaseStyle); + lastControl = wCaseStyle; + + // Word separator + Label wlWordSeparator = new Label(parent, SWT.RIGHT); + PropsUi.setLook(wlWordSeparator); + wlWordSeparator.setText(BaseMessages.getString(PKG, "NamingSchemeEditor.WordSeparator.Label")); + FormData fdlWordSeparator = new FormData(); + fdlWordSeparator.top = new FormAttachment(lastControl, margin); + fdlWordSeparator.left = new FormAttachment(0, 0); + fdlWordSeparator.right = new FormAttachment(middle, -margin); + wlWordSeparator.setLayoutData(fdlWordSeparator); + wWordSeparator = new Combo(parent, SWT.SINGLE | SWT.READ_ONLY | SWT.BORDER | SWT.LEFT); + PropsUi.setLook(wWordSeparator); + wWordSeparator.setItems(NamingWordSeparator.getLabels()); + FormData fdWordSeparator = new FormData(); + fdWordSeparator.top = new FormAttachment(wlWordSeparator, 0, SWT.CENTER); + fdWordSeparator.left = new FormAttachment(middle, 0); + fdWordSeparator.right = new FormAttachment(100, 0); + wWordSeparator.setLayoutData(fdWordSeparator); + lastControl = wWordSeparator; + + // Extra delimiters + Label wlExtra = new Label(parent, SWT.RIGHT); + PropsUi.setLook(wlExtra); + wlExtra.setText(BaseMessages.getString(PKG, "NamingSchemeEditor.ExtraDelimiters.Label")); + FormData fdlExtra = new FormData(); + fdlExtra.top = new FormAttachment(lastControl, margin); + fdlExtra.left = new FormAttachment(0, 0); + fdlExtra.right = new FormAttachment(middle, -margin); + wlExtra.setLayoutData(fdlExtra); + wExtraDelimiters = new Text(parent, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wExtraDelimiters); + FormData fdExtra = new FormData(); + fdExtra.top = new FormAttachment(wlExtra, 0, SWT.CENTER); + fdExtra.left = new FormAttachment(middle, 0); + fdExtra.right = new FormAttachment(100, 0); + wExtraDelimiters.setLayoutData(fdExtra); + lastControl = wExtraDelimiters; + + // Remove special characters + Label wlRemoveSpecial = new Label(parent, SWT.RIGHT); + PropsUi.setLook(wlRemoveSpecial); + wlRemoveSpecial.setText( + BaseMessages.getString(PKG, "NamingSchemeEditor.RemoveSpecialCharacters.Label")); + FormData fdlRemoveSpecial = new FormData(); + fdlRemoveSpecial.top = new FormAttachment(lastControl, margin); + fdlRemoveSpecial.left = new FormAttachment(0, 0); + fdlRemoveSpecial.right = new FormAttachment(middle, -margin); + wlRemoveSpecial.setLayoutData(fdlRemoveSpecial); + wRemoveSpecial = new Button(parent, SWT.CHECK | SWT.LEFT); + PropsUi.setLook(wRemoveSpecial); + FormData fdRemoveSpecial = new FormData(); + fdRemoveSpecial.top = new FormAttachment(wlRemoveSpecial, 0, SWT.CENTER); + fdRemoveSpecial.left = new FormAttachment(middle, 0); + fdRemoveSpecial.right = new FormAttachment(100, 0); + wRemoveSpecial.setLayoutData(fdRemoveSpecial); + lastControl = wlRemoveSpecial; + + // Collapse separators + Label wlCollapse = new Label(parent, SWT.RIGHT); + PropsUi.setLook(wlCollapse); + wlCollapse.setText(BaseMessages.getString(PKG, "NamingSchemeEditor.CollapseSeparators.Label")); + FormData fdlCollapse = new FormData(); + fdlCollapse.top = new FormAttachment(lastControl, margin); + fdlCollapse.left = new FormAttachment(0, 0); + fdlCollapse.right = new FormAttachment(middle, -margin); + wlCollapse.setLayoutData(fdlCollapse); + wCollapseSeparators = new Button(parent, SWT.CHECK | SWT.LEFT); + PropsUi.setLook(wCollapseSeparators); + FormData fdCollapse = new FormData(); + fdCollapse.top = new FormAttachment(wlCollapse, 0, SWT.CENTER); + fdCollapse.left = new FormAttachment(middle, 0); + fdCollapse.right = new FormAttachment(100, 0); + wCollapseSeparators.setLayoutData(fdCollapse); + lastControl = wlCollapse; + + // Trim edge separators + Label wlTrim = new Label(parent, SWT.RIGHT); + PropsUi.setLook(wlTrim); + wlTrim.setText(BaseMessages.getString(PKG, "NamingSchemeEditor.TrimEdgeSeparators.Label")); + FormData fdlTrim = new FormData(); + fdlTrim.top = new FormAttachment(lastControl, margin); + fdlTrim.left = new FormAttachment(0, 0); + fdlTrim.right = new FormAttachment(middle, -margin); + wlTrim.setLayoutData(fdlTrim); + wTrimEdges = new Button(parent, SWT.CHECK | SWT.LEFT); + PropsUi.setLook(wTrimEdges); + FormData fdTrim = new FormData(); + fdTrim.top = new FormAttachment(wlTrim, 0, SWT.CENTER); + fdTrim.left = new FormAttachment(middle, 0); + fdTrim.right = new FormAttachment(100, 0); + wTrimEdges.setLayoutData(fdTrim); + lastControl = wlTrim; + + // Prefix + Label wlPrefix = new Label(parent, SWT.RIGHT); + PropsUi.setLook(wlPrefix); + wlPrefix.setText(BaseMessages.getString(PKG, "NamingSchemeEditor.Prefix.Label")); + FormData fdlPrefix = new FormData(); + fdlPrefix.top = new FormAttachment(lastControl, margin); + fdlPrefix.left = new FormAttachment(0, 0); + fdlPrefix.right = new FormAttachment(middle, -margin); + wlPrefix.setLayoutData(fdlPrefix); + wPrefix = new Text(parent, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wPrefix); + FormData fdPrefix = new FormData(); + fdPrefix.top = new FormAttachment(wlPrefix, 0, SWT.CENTER); + fdPrefix.left = new FormAttachment(middle, 0); + fdPrefix.right = new FormAttachment(100, 0); + wPrefix.setLayoutData(fdPrefix); + lastControl = wPrefix; + + // Suffix + Label wlSuffix = new Label(parent, SWT.RIGHT); + PropsUi.setLook(wlSuffix); + wlSuffix.setText(BaseMessages.getString(PKG, "NamingSchemeEditor.Suffix.Label")); + FormData fdlSuffix = new FormData(); + fdlSuffix.top = new FormAttachment(lastControl, margin); + fdlSuffix.left = new FormAttachment(0, 0); + fdlSuffix.right = new FormAttachment(middle, -margin); + wlSuffix.setLayoutData(fdlSuffix); + wSuffix = new Text(parent, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wSuffix); + FormData fdSuffix = new FormData(); + fdSuffix.top = new FormAttachment(wlSuffix, 0, SWT.CENTER); + fdSuffix.left = new FormAttachment(middle, 0); + fdSuffix.right = new FormAttachment(100, 0); + wSuffix.setLayoutData(fdSuffix); + lastControl = wSuffix; + + // Preview input + Label wlPreviewInput = new Label(parent, SWT.RIGHT); + PropsUi.setLook(wlPreviewInput); + wlPreviewInput.setText(BaseMessages.getString(PKG, "NamingSchemeEditor.PreviewInput.Label")); + FormData fdlPreviewInput = new FormData(); + fdlPreviewInput.top = new FormAttachment(lastControl, margin * 2); + fdlPreviewInput.left = new FormAttachment(0, 0); + fdlPreviewInput.right = new FormAttachment(middle, -margin); + wlPreviewInput.setLayoutData(fdlPreviewInput); + wPreviewInput = new Text(parent, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wPreviewInput); + wPreviewInput.setText("Order ID"); + FormData fdPreviewInput = new FormData(); + fdPreviewInput.top = new FormAttachment(wlPreviewInput, 0, SWT.CENTER); + fdPreviewInput.left = new FormAttachment(middle, 0); + fdPreviewInput.right = new FormAttachment(100, 0); + wPreviewInput.setLayoutData(fdPreviewInput); + lastControl = wPreviewInput; + + // Preview output (read-only) + Label wlPreviewOutput = new Label(parent, SWT.RIGHT); + PropsUi.setLook(wlPreviewOutput); + wlPreviewOutput.setText(BaseMessages.getString(PKG, "NamingSchemeEditor.PreviewOutput.Label")); + FormData fdlPreviewOutput = new FormData(); + fdlPreviewOutput.top = new FormAttachment(lastControl, margin); + fdlPreviewOutput.left = new FormAttachment(0, 0); + fdlPreviewOutput.right = new FormAttachment(middle, -margin); + wlPreviewOutput.setLayoutData(fdlPreviewOutput); + wPreviewOutput = new Text(parent, SWT.SINGLE | SWT.LEFT | SWT.BORDER | SWT.READ_ONLY); + PropsUi.setLook(wPreviewOutput); + FormData fdPreviewOutput = new FormData(); + fdPreviewOutput.top = new FormAttachment(wlPreviewOutput, 0, SWT.CENTER); + fdPreviewOutput.left = new FormAttachment(middle, 0); + fdPreviewOutput.right = new FormAttachment(100, 0); + wPreviewOutput.setLayoutData(fdPreviewOutput); + + setWidgetsContent(); + updatePreview(); + + wName.addModifyListener(e -> setChanged()); + wDescription.addModifyListener(e -> setChanged()); + wType.addModifyListener(e -> setChanged()); + wCaseStyle.addModifyListener( + e -> { + setChanged(); + updatePreview(); + }); + wWordSeparator.addModifyListener( + e -> { + setChanged(); + updatePreview(); + }); + wExtraDelimiters.addModifyListener( + e -> { + setChanged(); + updatePreview(); + }); + wRemoveSpecial.addListener( + SWT.Selection, + e -> { + setChanged(); + updatePreview(); + }); + wCollapseSeparators.addListener( + SWT.Selection, + e -> { + setChanged(); + updatePreview(); + }); + wTrimEdges.addListener( + SWT.Selection, + e -> { + setChanged(); + updatePreview(); + }); + wPrefix.addModifyListener( + e -> { + setChanged(); + updatePreview(); + }); + wSuffix.addModifyListener( + e -> { + setChanged(); + updatePreview(); + }); + wPreviewInput.addModifyListener(e -> updatePreview()); + } + + private void updatePreview() { + if (wPreviewOutput == null || wPreviewOutput.isDisposed()) { + return; + } + NamingScheme preview = new NamingScheme(); + fillSchemeFromWidgets(preview); + String input = wPreviewInput != null ? wPreviewInput.getText() : ""; + wPreviewOutput.setText(Const.NVL(NamingEngine.apply(preview, input), "")); + } + + private void fillSchemeFromWidgets(NamingScheme scheme) { + scheme.setName(wName.getText()); + scheme.setDescription(wDescription.getText()); + scheme.setType(NamingSchemeType.fromLabel(wType.getText()).getCode()); + scheme.setCaseStyle(NamingCaseStyle.fromLabel(wCaseStyle.getText()).getCode()); + scheme.setWordSeparator(NamingWordSeparator.fromLabel(wWordSeparator.getText()).getCode()); + scheme.setExtraDelimiters(wExtraDelimiters.getText()); + scheme.setRemoveSpecialCharacters(wRemoveSpecial.getSelection()); + scheme.setCollapseRepeatedSeparators(wCollapseSeparators.getSelection()); + scheme.setTrimEdgeSeparators(wTrimEdges.getSelection()); + scheme.setPrefix(wPrefix.getText()); + scheme.setSuffix(wSuffix.getText()); + } + + @Override + public void setWidgetsContent() { + NamingScheme scheme = getMetadata(); + wName.setText(Const.NVL(scheme.getName(), "")); + wDescription.setText(Const.NVL(scheme.getDescription(), "")); + wType.setText(NamingSchemeType.fromCode(scheme.getType()).getLabel()); + wCaseStyle.setText(NamingCaseStyle.fromCode(scheme.getCaseStyle()).getLabel()); + wWordSeparator.setText(NamingWordSeparator.fromCode(scheme.getWordSeparator()).getLabel()); + wExtraDelimiters.setText(Const.NVL(scheme.getExtraDelimiters(), "")); + wRemoveSpecial.setSelection(scheme.isRemoveSpecialCharacters()); + wCollapseSeparators.setSelection(scheme.isCollapseRepeatedSeparators()); + wTrimEdges.setSelection(scheme.isTrimEdgeSeparators()); + wPrefix.setText(Const.NVL(scheme.getPrefix(), "")); + wSuffix.setText(Const.NVL(scheme.getSuffix(), "")); + } + + @Override + public void getWidgetsContent(NamingScheme scheme) { + fillSchemeFromWidgets(scheme); + } + + @Override + public boolean setFocus() { + if (wName == null || wName.isDisposed()) { + return false; + } + return wName.setFocus(); + } +} diff --git a/plugins/misc/naming/src/main/java/org/apache/hop/naming/metadata/NamingSchemeType.java b/plugins/misc/naming/src/main/java/org/apache/hop/naming/metadata/NamingSchemeType.java new file mode 100644 index 00000000000..c16bdb6193a --- /dev/null +++ b/plugins/misc/naming/src/main/java/org/apache/hop/naming/metadata/NamingSchemeType.java @@ -0,0 +1,92 @@ +/* + * 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.hop.naming.metadata; + +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.i18n.BaseMessages; + +/** + * Target kind for a {@link NamingScheme}. Stored as a stable string code on the metadata element. + */ +public enum NamingSchemeType { + HOP_FIELD("hop-field", "NamingSchemeType.HopField"), + DATABASE_TABLE("database-table", "NamingSchemeType.DatabaseTable"), + DATABASE_COLUMN("database-column", "NamingSchemeType.DatabaseColumn"), + FILE("file", "NamingSchemeType.File"), + FOLDER("folder", "NamingSchemeType.Folder"); + + private static final Class PKG = NamingScheme.class; + + private final String code; + private final String i18nKey; + + NamingSchemeType(String code, String i18nKey) { + this.code = code; + this.i18nKey = i18nKey; + } + + public String getCode() { + return code; + } + + public String getLabel() { + return BaseMessages.getString(PKG, i18nKey); + } + + public static NamingSchemeType fromCode(String code) { + if (StringUtils.isEmpty(code)) { + return HOP_FIELD; + } + for (NamingSchemeType type : values()) { + if (type.code.equalsIgnoreCase(code.trim())) { + return type; + } + } + return HOP_FIELD; + } + + public static String[] getCodes() { + NamingSchemeType[] values = values(); + String[] codes = new String[values.length]; + for (int i = 0; i < values.length; i++) { + codes[i] = values[i].code; + } + return codes; + } + + public static String[] getLabels() { + NamingSchemeType[] values = values(); + String[] labels = new String[values.length]; + for (int i = 0; i < values.length; i++) { + labels[i] = values[i].getLabel(); + } + return labels; + } + + public static NamingSchemeType fromLabel(String label) { + if (StringUtils.isEmpty(label)) { + return HOP_FIELD; + } + for (NamingSchemeType type : values()) { + if (type.getLabel().equals(label) || type.code.equalsIgnoreCase(label.trim())) { + return type; + } + } + return HOP_FIELD; + } +} diff --git a/plugins/misc/naming/src/main/java/org/apache/hop/naming/metadata/NamingWordSeparator.java b/plugins/misc/naming/src/main/java/org/apache/hop/naming/metadata/NamingWordSeparator.java new file mode 100644 index 00000000000..0ecd0e2c881 --- /dev/null +++ b/plugins/misc/naming/src/main/java/org/apache/hop/naming/metadata/NamingWordSeparator.java @@ -0,0 +1,85 @@ +/* + * 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.hop.naming.metadata; + +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.i18n.BaseMessages; + +/** Character used to join words after splitting (ignored for camelCase / PascalCase styles). */ +public enum NamingWordSeparator { + EMPTY("empty", "", "NamingWordSeparator.Empty"), + UNDERSCORE("underscore", "_", "NamingWordSeparator.Underscore"), + DASH("dash", "-", "NamingWordSeparator.Dash"); + + private static final Class PKG = NamingScheme.class; + + private final String code; + private final String value; + private final String i18nKey; + + NamingWordSeparator(String code, String value, String i18nKey) { + this.code = code; + this.value = value; + this.i18nKey = i18nKey; + } + + public String getCode() { + return code; + } + + public String getValue() { + return value; + } + + public String getLabel() { + return BaseMessages.getString(PKG, i18nKey); + } + + public static NamingWordSeparator fromCode(String code) { + if (StringUtils.isEmpty(code)) { + return UNDERSCORE; + } + for (NamingWordSeparator sep : values()) { + if (sep.code.equalsIgnoreCase(code.trim())) { + return sep; + } + } + return UNDERSCORE; + } + + public static String[] getLabels() { + NamingWordSeparator[] values = values(); + String[] labels = new String[values.length]; + for (int i = 0; i < values.length; i++) { + labels[i] = values[i].getLabel(); + } + return labels; + } + + public static NamingWordSeparator fromLabel(String label) { + if (StringUtils.isEmpty(label)) { + return UNDERSCORE; + } + for (NamingWordSeparator sep : values()) { + if (sep.getLabel().equals(label) || sep.code.equalsIgnoreCase(label.trim())) { + return sep; + } + } + return UNDERSCORE; + } +} diff --git a/plugins/misc/naming/src/main/resources/naming.svg b/plugins/misc/naming/src/main/resources/naming.svg new file mode 100644 index 00000000000..8c693fbbf0d --- /dev/null +++ b/plugins/misc/naming/src/main/resources/naming.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugins/misc/naming/src/main/resources/org/apache/hop/naming/gui/messages/messages_en_US.properties b/plugins/misc/naming/src/main/resources/org/apache/hop/naming/gui/messages/messages_en_US.properties new file mode 100644 index 00000000000..0bff16b34aa --- /dev/null +++ b/plugins/misc/naming/src/main/resources/org/apache/hop/naming/gui/messages/messages_en_US.properties @@ -0,0 +1,28 @@ +# +# +# 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. +# +# + +Naming.Toolbar.Apply.ToolTip=Apply a Naming Scheme to a column +Naming.ColumnSelection.Title=Select column +Naming.ColumnSelection.Message=Select the column whose values should be renamed +Naming.SchemeSelection.Title=Select Naming Scheme +Naming.SchemeSelection.Message=Select the Naming Scheme to apply (type\: Hop field names) +Naming.NoSchemes.Title=No Naming Schemes +Naming.NoSchemes.Message=There are no Naming Scheme metadata elements of type "Hop field names".\nCreate one in the Metadata perspective first. +Naming.Error.Title=Error +Naming.Error.Message=Error applying Naming Scheme diff --git a/plugins/misc/naming/src/main/resources/org/apache/hop/naming/metadata/messages/messages_en_US.properties b/plugins/misc/naming/src/main/resources/org/apache/hop/naming/metadata/messages/messages_en_US.properties new file mode 100644 index 00000000000..73903c5662e --- /dev/null +++ b/plugins/misc/naming/src/main/resources/org/apache/hop/naming/metadata/messages/messages_en_US.properties @@ -0,0 +1,51 @@ +# +# +# 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. +# +# + +NamingScheme.Name=Naming Scheme +NamingScheme.Description=Reusable rules to normalize names for Hop fields, database tables/columns, and files/folders + +NamingSchemeEditor.Name.Label=Name +NamingSchemeEditor.Description.Label=Description +NamingSchemeEditor.Type.Label=Type +NamingSchemeEditor.CaseStyle.Label=Case style +NamingSchemeEditor.WordSeparator.Label=Word separator +NamingSchemeEditor.ExtraDelimiters.Label=Extra delimiters +NamingSchemeEditor.RemoveSpecialCharacters.Label=Remove special characters +NamingSchemeEditor.CollapseSeparators.Label=Collapse repeated separators +NamingSchemeEditor.TrimEdgeSeparators.Label=Trim leading/trailing separators +NamingSchemeEditor.Prefix.Label=Prefix +NamingSchemeEditor.Suffix.Label=Suffix +NamingSchemeEditor.PreviewInput.Label=Preview input +NamingSchemeEditor.PreviewOutput.Label=Preview output + +NamingSchemeType.HopField=Hop field names +NamingSchemeType.DatabaseTable=Database tables +NamingSchemeType.DatabaseColumn=Database columns +NamingSchemeType.File=Files +NamingSchemeType.Folder=Folders + +NamingCaseStyle.AsIs=As is +NamingCaseStyle.Lower=Lower case +NamingCaseStyle.Upper=Upper case +NamingCaseStyle.Camel=camelCase +NamingCaseStyle.Pascal=PascalCase + +NamingWordSeparator.Empty=None (empty) +NamingWordSeparator.Underscore=Underscore (_) +NamingWordSeparator.Dash=Dash (-) diff --git a/plugins/misc/naming/src/test/java/org/apache/hop/naming/engine/NamingEngineTest.java b/plugins/misc/naming/src/test/java/org/apache/hop/naming/engine/NamingEngineTest.java new file mode 100644 index 00000000000..97e1dc38e95 --- /dev/null +++ b/plugins/misc/naming/src/test/java/org/apache/hop/naming/engine/NamingEngineTest.java @@ -0,0 +1,131 @@ +/* + * 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.hop.naming.engine; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.apache.hop.naming.metadata.NamingCaseStyle; +import org.apache.hop.naming.metadata.NamingScheme; +import org.apache.hop.naming.metadata.NamingWordSeparator; +import org.junit.jupiter.api.Test; + +class NamingEngineTest { + + @Test + void nullAndEmpty() { + NamingScheme scheme = lowerUnderscore(); + assertNull(NamingEngine.apply(scheme, null)); + assertEquals("", NamingEngine.apply(scheme, "")); + } + + @Test + void orderIdLowerUnderscore() { + assertEquals("order_id", NamingEngine.apply(lowerUnderscore(), "Order ID")); + } + + @Test + void orderIdAsIsUnderscore() { + NamingScheme scheme = new NamingScheme(); + scheme.setCaseStyle(NamingCaseStyle.AS_IS.getCode()); + scheme.setWordSeparator(NamingWordSeparator.UNDERSCORE.getCode()); + scheme.setRemoveSpecialCharacters(false); + assertEquals("Order_ID", NamingEngine.apply(scheme, "Order ID")); + } + + @Test + void orderIdCamel() { + NamingScheme scheme = new NamingScheme(); + scheme.setCaseStyle(NamingCaseStyle.CAMEL.getCode()); + scheme.setRemoveSpecialCharacters(false); + assertEquals("orderId", NamingEngine.apply(scheme, "Order ID")); + } + + @Test + void orderIdPascal() { + NamingScheme scheme = new NamingScheme(); + scheme.setCaseStyle(NamingCaseStyle.PASCAL.getCode()); + scheme.setRemoveSpecialCharacters(false); + assertEquals("OrderId", NamingEngine.apply(scheme, "Order ID")); + } + + @Test + void snakeToCamel() { + NamingScheme scheme = new NamingScheme(); + scheme.setCaseStyle(NamingCaseStyle.CAMEL.getCode()); + assertEquals("orderId", NamingEngine.apply(scheme, "order_id")); + } + + @Test + void kebabToUpperSnake() { + NamingScheme scheme = new NamingScheme(); + scheme.setCaseStyle(NamingCaseStyle.UPPER.getCode()); + scheme.setWordSeparator(NamingWordSeparator.UNDERSCORE.getCode()); + assertEquals("ORDER_ID", NamingEngine.apply(scheme, "order-id")); + } + + @Test + void collapseAndTrimWhitespace() { + assertEquals("foo_bar", NamingEngine.apply(lowerUnderscore(), " foo bar ")); + } + + @Test + void removeSpecialCharacters() { + NamingScheme scheme = lowerUnderscore(); + scheme.setRemoveSpecialCharacters(true); + scheme.setExtraDelimiters("#"); + assertEquals("field_name", NamingEngine.apply(scheme, "field#name!")); + } + + @Test + void dashSeparator() { + NamingScheme scheme = new NamingScheme(); + scheme.setCaseStyle(NamingCaseStyle.LOWER.getCode()); + scheme.setWordSeparator(NamingWordSeparator.DASH.getCode()); + assertEquals("order-id", NamingEngine.apply(scheme, "Order ID")); + } + + @Test + void prefixAndSuffix() { + NamingScheme scheme = lowerUnderscore(); + scheme.setPrefix("fld_"); + scheme.setSuffix("_x"); + assertEquals("fld_order_id_x", NamingEngine.apply(scheme, "Order ID")); + } + + @Test + void camelCaseInputSplit() { + NamingScheme scheme = lowerUnderscore(); + assertEquals("order_id", NamingEngine.apply(scheme, "orderId")); + } + + @Test + void nullSchemeUsesDefaults() { + assertEquals("order_id", NamingEngine.apply(null, "Order ID")); + } + + private static NamingScheme lowerUnderscore() { + NamingScheme scheme = new NamingScheme(); + scheme.setCaseStyle(NamingCaseStyle.LOWER.getCode()); + scheme.setWordSeparator(NamingWordSeparator.UNDERSCORE.getCode()); + scheme.setRemoveSpecialCharacters(true); + scheme.setCollapseRepeatedSeparators(true); + scheme.setTrimEdgeSeparators(true); + return scheme; + } +} diff --git a/plugins/misc/pom.xml b/plugins/misc/pom.xml index 83374e5c95a..a6b5122f120 100644 --- a/plugins/misc/pom.xml +++ b/plugins/misc/pom.xml @@ -37,6 +37,7 @@ import mail marketplace + naming passwords projects py4j diff --git a/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java b/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java index 96f2ba39df3..daf14df1a72 100644 --- a/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java +++ b/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java @@ -4229,6 +4229,59 @@ public void setText(String text, int colNr, int rowNr) { item.setText(colNr, text); } + /** + * Replace cell values in one column for the given rows and record a single undo step. + * + *

{@code colNr} is the {@link TableItem} column index (1 = first {@link ColumnInfo}; 0 is the + * leading row-number column). + * + * @param colNr table item column index + * @param rowIndices absolute row indices into {@link #table} + * @param newValues new cell values, same length as {@code rowIndices} + */ + public void applyColumnValues(int colNr, int[] rowIndices, String[] newValues) { + if (readonly || rowIndices == null || newValues == null || rowIndices.length == 0) { + return; + } + if (rowIndices.length != newValues.length) { + throw new IllegalArgumentException( + "rowIndices and newValues must have the same length (" + + rowIndices.length + + " vs " + + newValues.length + + ")"); + } + if (colNr < 1 || colNr >= table.getColumnCount()) { + return; + } + + applyAllChanges(); + + int size = rowIndices.length; + String[][] before = new String[size][]; + String[][] after = new String[size][]; + int[] index = new int[size]; + + for (int i = 0; i < size; i++) { + int rowNr = rowIndices[i]; + if (rowNr < 0 || rowNr >= table.getItemCount()) { + continue; + } + TableItem item = table.getItem(rowNr); + index[i] = rowNr; + before[i] = getItemText(item); + item.setText(colNr, Const.NVL(newValues[i], "")); + after[i] = getItemText(item); + } + + if (undoEnabled) { + ChangeAction ta = new ChangeAction(); + ta.setChanged(before, after, index); + addUndo(ta); + } + setModified(); + } + /** * @param sortable the sortable to set */