/*******************************************************************************
*
* Pentaho Data Integration
*
* Copyright (C) 2002-2016 by Pentaho : http://www.pentaho.com
*
*******************************************************************************
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/
package org.pentaho.di.ui.core.dialog;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.ShellAdapter;
import org.eclipse.swt.events.ShellEvent;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Dialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.List;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.pentaho.di.core.Const;
import org.pentaho.di.core.SourceToTargetMapping;
import org.pentaho.di.i18n.BaseMessages;
import org.pentaho.di.ui.core.PropsUI;
import org.pentaho.di.ui.core.database.dialog.DatabaseDialog;
import org.pentaho.di.ui.core.gui.GUIResource;
import org.pentaho.di.ui.core.gui.WindowProperty;
import org.pentaho.di.ui.trans.step.BaseStepDialog;
/**
* Shows a user 2 lists of strings and allows the linkage of values between values in the 2 lists
*
* @author Matt
* @since 23-03-2006
*/
public class EnterMappingDialog extends Dialog {
private static Class<?> PKG = DatabaseDialog.class; // for i18n purposes, needed by Translator2!!
public class GuessPair {
private int _srcIndex = -1;
private int _targetIndex = -1;
private boolean _found = false;
public GuessPair() {
_found = false;
}
public GuessPair( int src ) {
_srcIndex = src;
_found = false;
}
public GuessPair( int src, int target, boolean found ) {
_srcIndex = src;
_targetIndex = target;
_found = found;
}
public GuessPair( int src, int target ) {
_srcIndex = src;
_targetIndex = target;
_found = true;
}
public int getTargetIndex() {
return _targetIndex;
}
public void setTargetIndex( int targetIndex ) {
_found = true;
_targetIndex = targetIndex;
}
public int getSrcIndex() {
return _srcIndex;
}
public void setSrcIndex( int srcIndex ) {
_srcIndex = srcIndex;
}
public boolean getFound() {
return _found;
}
}
public static final String STRING_ORIGIN_SEPARATOR = " (";
private Label wlSource;
public static final String STRING_SFORCE_EXTERNALID_SEPARATOR = "/";
private List wSource;
private FormData fdlSource, fdSource;
private Label wlSourceAuto;
private Button wSourceAuto;
private FormData fdlSourceAuto, fdSourceAuto;
private Label wlSourceHide;
private Button wSourceHide;
private FormData fdlSourceHide, fdSourceHide;
private Label wlTarget;
private List wTarget;
private FormData fdlTarget, fdTarget;
private Label wlTargetAuto;
private Button wTargetAuto;
private FormData fdlTargetAuto, fdTargetAuto;
private Label wlTargetHide;
private Button wTargetHide;
private FormData fdlTargetHide, fdTargetHide;
private Label wlResult;
private List wResult;
private FormData fdlResult, fdResult;
private Button wAdd;
private FormData fdAdd;
private Button wDelete;
private FormData fdDelete;
private Button wOK, wGuess, wCancel;
private Listener lsOK, lsGuess, lsCancel;
private Shell shell;
private String[] sourceList;
private String[] targetList;
private PropsUI props;
private java.util.List<SourceToTargetMapping> mappings;
/**
* Create a new dialog allowing the user to enter a mapping
*
* @param parent
* the parent shell
* @param source
* the source values
* @param target
* the target values
*/
public EnterMappingDialog( Shell parent, String[] source, String[] target ) {
this( parent, source, target, new ArrayList<SourceToTargetMapping>() );
}
/**
* Create a new dialog allowing the user to enter a mapping
*
* @param parent
* the parent shell
* @param source
* the source values
* @param target
* the target values
* @param mappings
* the already selected mappings (ArrayList containing <code>SourceToTargetMapping</code>s)
*/
public EnterMappingDialog( Shell parent, String[] source, String[] target,
java.util.List<SourceToTargetMapping> mappings ) {
super( parent, SWT.NONE );
props = PropsUI.getInstance();
this.sourceList = source;
this.targetList = target;
this.mappings = mappings;
}
public java.util.List<SourceToTargetMapping> open() {
Shell parent = getParent();
Display display = parent.getDisplay();
shell =
new Shell( parent, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MIN | SWT.MAX | SWT.APPLICATION_MODAL | SWT.SHEET );
props.setLook( shell );
shell.setImage( GUIResource.getInstance().getImageSpoon() );
FormLayout formLayout = new FormLayout();
formLayout.marginWidth = Const.FORM_MARGIN;
formLayout.marginHeight = Const.FORM_MARGIN;
shell.setLayout( formLayout );
shell.setText( BaseMessages.getString( PKG, "EnterMappingDialog.Title" ) );
shell.setImage( GUIResource.getInstance().getImageTransGraph() );
int margin = Const.MARGIN;
int buttonSpace = 90;
// Source table
wlSource = new Label( shell, SWT.NONE );
wlSource.setText( BaseMessages.getString( PKG, "EnterMappingDialog.SourceFields.Label" ) );
props.setLook( wlSource );
fdlSource = new FormData();
fdlSource.left = new FormAttachment( 0, 0 );
fdlSource.top = new FormAttachment( 0, margin );
wlSource.setLayoutData( fdlSource );
wSource = new List( shell, SWT.SINGLE | SWT.RIGHT | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL );
for ( int i = 0; i < sourceList.length; i++ ) {
wSource.add( sourceList[i] );
}
props.setLook( wSource );
fdSource = new FormData();
fdSource.left = new FormAttachment( 0, 0 );
fdSource.right = new FormAttachment( 25, 0 );
fdSource.top = new FormAttachment( wlSource, margin );
fdSource.bottom = new FormAttachment( 100, -buttonSpace );
wSource.setLayoutData( fdSource );
// Automatic target selection
wlSourceAuto = new Label( shell, SWT.NONE );
wlSourceAuto.setText( BaseMessages.getString( PKG, "EnterMappingDialog.AutoTargetSelection.Label" ) );
props.setLook( wlSourceAuto );
fdlSourceAuto = new FormData();
fdlSourceAuto.left = new FormAttachment( 0, 0 );
fdlSourceAuto.top = new FormAttachment( wSource, margin );
wlSourceAuto.setLayoutData( fdlSourceAuto );
wSourceAuto = new Button( shell, SWT.CHECK );
wSourceAuto.setSelection( true );
props.setLook( wSourceAuto );
fdSourceAuto = new FormData();
fdSourceAuto.left = new FormAttachment( wlSourceAuto, margin * 2 );
fdSourceAuto.right = new FormAttachment( 25, 0 );
fdSourceAuto.top = new FormAttachment( wSource, margin );
wSourceAuto.setLayoutData( fdSourceAuto );
// Hide used source fields?
wlSourceHide = new Label( shell, SWT.NONE );
wlSourceHide.setText( BaseMessages.getString( PKG, "EnterMappingDialog.HideUsedSources" ) );
props.setLook( wlSourceHide );
fdlSourceHide = new FormData();
fdlSourceHide.left = new FormAttachment( 0, 0 );
fdlSourceHide.top = new FormAttachment( wSourceAuto, margin );
wlSourceHide.setLayoutData( fdlSourceHide );
wSourceHide = new Button( shell, SWT.CHECK );
wSourceHide.setSelection( true );
props.setLook( wSourceHide );
fdSourceHide = new FormData();
fdSourceHide.left = new FormAttachment( wlSourceHide, margin * 2 );
fdSourceHide.right = new FormAttachment( 25, 0 );
fdSourceHide.top = new FormAttachment( wSourceAuto, margin );
wSourceHide.setLayoutData( fdSourceHide );
wSourceHide.addSelectionListener( new SelectionAdapter() {
@Override
public void widgetSelected( SelectionEvent e ) {
refreshMappings();
}
} );
// Target table
wlTarget = new Label( shell, SWT.NONE );
wlTarget.setText( BaseMessages.getString( PKG, "EnterMappingDialog.TargetFields.Label" ) );
props.setLook( wlTarget );
fdlTarget = new FormData();
fdlTarget.left = new FormAttachment( wSource, margin * 2 );
fdlTarget.top = new FormAttachment( 0, margin );
wlTarget.setLayoutData( fdlTarget );
wTarget = new List( shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL );
for ( int i = 0; i < targetList.length; i++ ) {
wTarget.add( targetList[i] );
}
props.setLook( wTarget );
fdTarget = new FormData();
fdTarget.left = new FormAttachment( wSource, margin * 2 );
fdTarget.right = new FormAttachment( 50, 0 );
fdTarget.top = new FormAttachment( wlTarget, margin );
fdTarget.bottom = new FormAttachment( 100, -buttonSpace );
wTarget.setLayoutData( fdTarget );
// Automatic target selection
wlTargetAuto = new Label( shell, SWT.NONE );
wlTargetAuto.setText( BaseMessages.getString( PKG, "EnterMappingDialog.AutoSourceSelection.Label" ) );
props.setLook( wlTargetAuto );
fdlTargetAuto = new FormData();
fdlTargetAuto.left = new FormAttachment( wSource, margin * 2 );
fdlTargetAuto.top = new FormAttachment( wTarget, margin );
wlTargetAuto.setLayoutData( fdlTargetAuto );
wTargetAuto = new Button( shell, SWT.CHECK );
wTargetAuto.setSelection( false );
props.setLook( wTargetAuto );
fdTargetAuto = new FormData();
fdTargetAuto.left = new FormAttachment( wlTargetAuto, margin * 2 );
fdTargetAuto.right = new FormAttachment( 50, 0 );
fdTargetAuto.top = new FormAttachment( wTarget, margin );
wTargetAuto.setLayoutData( fdTargetAuto );
// Automatic target selection
wlTargetHide = new Label( shell, SWT.NONE );
wlTargetHide.setText( BaseMessages.getString( PKG, "EnterMappingDialog.HideUsedTargets" ) );
props.setLook( wlTargetHide );
fdlTargetHide = new FormData();
fdlTargetHide.left = new FormAttachment( wSource, margin * 2 );
fdlTargetHide.top = new FormAttachment( wTargetAuto, margin );
wlTargetHide.setLayoutData( fdlTargetHide );
wTargetHide = new Button( shell, SWT.CHECK );
wTargetHide.setSelection( true );
props.setLook( wTargetHide );
fdTargetHide = new FormData();
fdTargetHide.left = new FormAttachment( wlTargetHide, margin * 2 );
fdTargetHide.right = new FormAttachment( 50, 0 );
fdTargetHide.top = new FormAttachment( wTargetAuto, margin );
wTargetHide.setLayoutData( fdTargetHide );
wTargetHide.addSelectionListener( new SelectionAdapter() {
@Override
public void widgetSelected( SelectionEvent e ) {
refreshMappings();
}
} );
// Add a couple of buttons:
wAdd = new Button( shell, SWT.PUSH );
fdAdd = new FormData();
wAdd.setText( BaseMessages.getString( PKG, "EnterMappingDialog.Button.Add" ) );
fdAdd.left = new FormAttachment( wTarget, margin * 2 );
fdAdd.top = new FormAttachment( wTarget, 0, SWT.CENTER );
wAdd.setLayoutData( fdAdd );
Listener lsAdd = new Listener() {
@Override
public void handleEvent( Event e ) {
add();
}
};
wAdd.addListener( SWT.Selection, lsAdd );
// Delete a couple of buttons:
wDelete = new Button( shell, SWT.PUSH );
fdDelete = new FormData();
wDelete.setText( BaseMessages.getString( PKG, "EnterMappingDialog.Button.Delete" ) );
fdDelete.left = new FormAttachment( wTarget, margin * 2 );
fdDelete.top = new FormAttachment( wAdd, margin * 2 );
wDelete.setLayoutData( fdDelete );
Listener lsDelete = new Listener() {
@Override
public void handleEvent( Event e ) {
delete();
}
};
wDelete.addListener( SWT.Selection, lsDelete );
// Result table
wlResult = new Label( shell, SWT.NONE );
wlResult.setText( BaseMessages.getString( PKG, "EnterMappingDialog.ResultMappings.Label" ) );
props.setLook( wlResult );
fdlResult = new FormData();
fdlResult.left = new FormAttachment( wDelete, margin * 2 );
fdlResult.top = new FormAttachment( 0, margin );
wlResult.setLayoutData( fdlResult );
wResult = new List( shell, SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL );
for ( int i = 0; i < targetList.length; i++ ) {
wResult.add( targetList[i] );
}
props.setLook( wResult );
fdResult = new FormData();
fdResult.left = new FormAttachment( wDelete, margin * 2 );
fdResult.right = new FormAttachment( 100, 0 );
fdResult.top = new FormAttachment( wlResult, margin );
fdResult.bottom = new FormAttachment( 100, -30 );
wResult.setLayoutData( fdResult );
// Some buttons
wOK = new Button( shell, SWT.PUSH );
wOK.setText( BaseMessages.getString( PKG, "System.Button.OK" ) );
lsOK = new Listener() {
@Override
public void handleEvent( Event e ) {
ok();
}
};
wOK.addListener( SWT.Selection, lsOK );
// Some buttons
wGuess = new Button( shell, SWT.PUSH );
wGuess.setText( BaseMessages.getString( PKG, "EnterMappingDialog.Button.Guess" ) );
lsGuess = new Listener() {
@Override
public void handleEvent( Event e ) {
guess();
}
};
wGuess.addListener( SWT.Selection, lsGuess );
wCancel = new Button( shell, SWT.PUSH );
wCancel.setText( BaseMessages.getString( PKG, "System.Button.Cancel" ) );
lsCancel = new Listener() {
@Override
public void handleEvent( Event e ) {
cancel();
}
};
wCancel.addListener( SWT.Selection, lsCancel );
BaseStepDialog.positionBottomButtons( shell, new Button[] { wOK, wGuess, wCancel }, margin, null );
wSource.addSelectionListener( new SelectionAdapter() {
@Override
public void widgetSelected( SelectionEvent e ) {
if ( wSourceAuto.getSelection() ) {
findTarget();
}
}
@Override
public void widgetDefaultSelected( SelectionEvent e ) {
add();
}
} );
wTarget.addSelectionListener( new SelectionAdapter() {
@Override
public void widgetSelected( SelectionEvent e ) {
if ( wTargetAuto.getSelection() ) {
findSource();
}
}
@Override
public void widgetDefaultSelected( SelectionEvent e ) {
add();
}
} );
// Detect [X] or ALT-F4 or something that kills this window...
shell.addShellListener( new ShellAdapter() {
@Override
public void shellClosed( ShellEvent e ) {
cancel();
}
} );
getData();
BaseStepDialog.setSize( shell );
shell.open();
while ( !shell.isDisposed() ) {
if ( !display.readAndDispatch() ) {
display.sleep();
}
}
return mappings;
}
private void guess() {
// Guess the target for all the sources...
String[] sortedSourceList = Arrays.copyOf( sourceList, sourceList.length );
// Sort Longest to Shortest string - makes matching better
Arrays.sort( sortedSourceList, new Comparator<String>() {
@Override
public int compare( String s1, String s2 ) {
return s2.length() - s1.length();
}
} );
// Look for matches using longest field name to shortest
ArrayList<GuessPair> pList = new ArrayList<GuessPair>();
for ( int i = 0; i < sourceList.length; i++ ) {
int idx = Const.indexOfString( sortedSourceList[i], wSource.getItems() );
if ( idx >= 0 ) {
pList.add( findTargetPair( idx ) );
}
}
// Now add them in order or source field list
Collections.sort( pList, new Comparator<GuessPair>() {
@Override
public int compare( GuessPair s1, GuessPair s2 ) {
return s1.getSrcIndex() - s2.getSrcIndex();
}
} );
for ( GuessPair p : pList ) {
if ( p.getFound() ) {
SourceToTargetMapping mapping = new SourceToTargetMapping( p.getSrcIndex(), p.getTargetIndex() );
mappings.add( mapping );
}
}
refreshMappings();
}
private boolean findTarget() {
int sourceIndex = wSource.getSelectionIndex();
GuessPair p = findTargetPair( sourceIndex );
if ( p.getFound() ) {
wTarget.setSelection( p.getTargetIndex() );
}
return p.getFound();
}
private GuessPair findTargetPair( int sourceIndex ) {
// Guess, user selects an entry in the list on the left.
// Find a comparable entry in the target list...
GuessPair result = new GuessPair( sourceIndex );
if ( sourceIndex < 0 ) {
return result; // Not Found
}
// Skip everything after the bracket...
String sourceStr = wSource.getItem( sourceIndex ).toUpperCase();
int indexOfBracket = sourceStr.indexOf( EnterMappingDialog.STRING_ORIGIN_SEPARATOR );
String sourceString = sourceStr;
if ( indexOfBracket >= 0 ) {
sourceString = sourceStr.substring( 0, indexOfBracket );
}
int length = sourceString.length();
boolean first = true;
boolean found = false;
while ( !found && ( length >= ( (int) ( sourceString.length() * 0.85 ) ) || first ) ) {
first = false;
for ( int i = 0; i < wTarget.getItemCount() && !found; i++ ) {
String test = wTarget.getItem( i ).toUpperCase();
// Clean up field names in the form of OBJECT:LOOKUPFIELD/OBJECTNAME
if ( test.contains( EnterMappingDialog.STRING_SFORCE_EXTERNALID_SEPARATOR ) ) {
String[] tmp = test.split( EnterMappingDialog.STRING_SFORCE_EXTERNALID_SEPARATOR );
test = tmp[tmp.length - 1];
if ( test.endsWith( "__R" ) ) {
test = test.substring( 0, test.length() - 3 ) + "__C";
}
}
if ( test.indexOf( sourceString.substring( 0, length ) ) >= 0 ) {
result.setSrcIndex( sourceIndex );
result.setTargetIndex( i );
found = true;
}
}
length--;
}
return result;
}
private boolean findSource() {
// Guess, user selects an entry in the list on the right.
// Find a comparable entry in the source list...
boolean found = false;
int targetIndex = wTarget.getSelectionIndex();
// Skip everything after the bracket...
String targetString = wTarget.getItem( targetIndex ).toUpperCase();
int length = targetString.length();
boolean first = true;
while ( !found && ( length >= 2 || first ) ) {
first = false;
for ( int i = 0; i < wSource.getItemCount() && !found; i++ ) {
if ( wSource.getItem( i ).toUpperCase().indexOf( targetString.substring( 0, length ) ) >= 0 ) {
wSource.setSelection( i );
found = true;
}
}
length--;
}
return found;
}
private void add() {
if ( wSource.getSelectionCount() == 1 && wTarget.getSelectionCount() == 1 ) {
String sourceString = wSource.getSelection()[0];
String targetString = wTarget.getSelection()[0];
int srcIndex = Const.indexOfString( sourceString, sourceList );
int tgtIndex = Const.indexOfString( targetString, targetList );
if ( srcIndex >= 0 && tgtIndex >= 0 ) {
// New mapping: add it to the list...
SourceToTargetMapping mapping = new SourceToTargetMapping( srcIndex, tgtIndex );
mappings.add( mapping );
refreshMappings();
}
}
}
private void refreshMappings() {
// Refresh the results...
wResult.removeAll();
for ( int i = 0; i < mappings.size(); i++ ) {
SourceToTargetMapping mapping = mappings.get( i );
String mappingString =
sourceList[mapping.getSourcePosition()] + " --> " + targetList[mapping.getTargetPosition()];
wResult.add( mappingString );
}
wSource.removeAll();
// Refresh the sources
for ( int a = 0; a < sourceList.length; a++ ) {
boolean found = false;
if ( wSourceHide.getSelection() ) {
for ( int b = 0; b < mappings.size() && !found; b++ ) {
SourceToTargetMapping mapping = mappings.get( b );
if ( mapping.getSourcePosition() == Const.indexOfString( sourceList[a], sourceList ) ) {
found = true;
}
}
}
if ( !found ) {
wSource.add( sourceList[a] );
}
}
wTarget.removeAll();
// Refresh the targets
for ( int a = 0; a < targetList.length; a++ ) {
boolean found = false;
if ( wTargetHide.getSelection() ) {
for ( int b = 0; b < mappings.size() && !found; b++ ) {
SourceToTargetMapping mapping = mappings.get( b );
if ( mapping.getTargetPosition() == Const.indexOfString( targetList[a], targetList ) ) {
found = true;
}
}
}
if ( !found ) {
wTarget.add( targetList[a] );
}
}
}
private void delete() {
String[] result = wResult.getSelection();
for ( int i = result.length - 1; i >= 0; i-- ) {
int idx = wResult.indexOf( result[i] );
if ( idx >= 0 && idx < mappings.size() ) {
mappings.remove( idx );
}
}
refreshMappings();
}
public void dispose() {
props.setScreen( new WindowProperty( shell ) );
shell.dispose();
}
public void getData() {
refreshMappings();
}
private void cancel() {
mappings = null;
dispose();
}
private void ok() {
dispose();
}
}