/*
 *  soapUI, copyright (C) 2004-2012 smartbear.com 
 *
 *  soapUI is free software; you can redistribute it and/or modify it under the 
 *  terms of version 2.1 of the GNU Lesser General Public License as published by 
 *  the Free Software Foundation.
 *
 *  soapUI 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 at gnu.org.
 */
package com.eviware.soapui.security.assertion;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JPanel;
import javax.swing.JScrollPane;

import org.apache.xmlbeans.XmlObject;
import org.jdesktop.swingx.JXTable;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.config.TestAssertionConfig;
import com.eviware.soapui.impl.support.AbstractHttpRequest;
import com.eviware.soapui.impl.wsdl.panels.assertions.AssertionCategoryMapping;
import com.eviware.soapui.impl.wsdl.panels.assertions.AssertionListEntry;
import com.eviware.soapui.impl.wsdl.support.HelpUrls;
import com.eviware.soapui.impl.wsdl.teststeps.WsdlMessageAssertion;
import com.eviware.soapui.impl.wsdl.teststeps.assertions.AbstractTestAssertionFactory;
import com.eviware.soapui.model.TestPropertyHolder;
import com.eviware.soapui.model.iface.MessageExchange;
import com.eviware.soapui.model.iface.SubmitContext;
import com.eviware.soapui.model.security.SecurityScan;
import com.eviware.soapui.model.security.SensitiveInformationTableModel;
import com.eviware.soapui.model.testsuite.Assertable;
import com.eviware.soapui.model.testsuite.AssertionError;
import com.eviware.soapui.model.testsuite.AssertionException;
import com.eviware.soapui.model.testsuite.ResponseAssertion;
import com.eviware.soapui.model.testsuite.TestProperty;
import com.eviware.soapui.security.SensitiveInformationPropertyHolder;
import com.eviware.soapui.support.SecurityScanUtil;
import com.eviware.soapui.support.StringUtils;
import com.eviware.soapui.support.UISupport;
import com.eviware.soapui.support.components.JXToolBar;
import com.eviware.soapui.support.xml.XmlObjectConfigurationBuilder;
import com.eviware.soapui.support.xml.XmlObjectConfigurationReader;
import com.eviware.x.form.XFormDialog;
import com.eviware.x.form.support.ADialogBuilder;
import com.eviware.x.form.support.AField;
import com.eviware.x.form.support.AField.AFieldType;
import com.eviware.x.form.support.AForm;

public class SensitiveInfoExposureAssertion extends WsdlMessageAssertion implements ResponseAssertion
{
	private static final String PREFIX = "~";
	public static final String ID = "Sensitive Information Exposure";
	public static final String LABEL = "Sensitive Information Exposure";

	private List<String> assertionSpecificExposureList;

	private XFormDialog dialog;
	private static final String ASSERTION_SPECIFIC_EXPOSURE_LIST = "AssertionSpecificExposureList";
	private static final String INCLUDE_GLOBAL = "IncludeGlobal";
	private static final String INCLUDE_PROJECT_SPECIFIC = "IncludeProjectSpecific";
	public static final String DESCRIPTION = "Checks that the last received message does not expose an sensitive information about the target system. Applicable to REST, SOAP and HTTP TestSteps.";
	private boolean includeGlobal;
	private boolean includeProjectSpecific;
	private JPanel sensitiveInfoTableForm;
	private SensitiveInformationTableModel sensitiveInformationTableModel;
	private JXTable tokenTable;

	public SensitiveInfoExposureAssertion( TestAssertionConfig assertionConfig, Assertable assertable )
	{
		super( assertionConfig, assertable, false, true, false, true );

		init();
	}

	private void init()
	{
		XmlObjectConfigurationReader reader = new XmlObjectConfigurationReader( getConfiguration() );
		includeGlobal = reader.readBoolean( INCLUDE_GLOBAL, true );
		includeProjectSpecific = reader.readBoolean( INCLUDE_PROJECT_SPECIFIC, true );
		assertionSpecificExposureList = StringUtils.toStringList( reader.readStrings( ASSERTION_SPECIFIC_EXPOSURE_LIST ) );
		extractTokenTable();
	}

	private void extractTokenTable()
	{
		SensitiveInformationPropertyHolder siph = new SensitiveInformationPropertyHolder();
		for( String str : assertionSpecificExposureList )
		{
			if ( "###".equals( str ) )
				continue;
			String[] tokens = str.split( "###" );
			if( tokens.length == 2 )
			{
				siph.setPropertyValue( tokens[0], tokens[1] );
			}
			else if( tokens.length == 1 )
			{
				siph.setPropertyValue( tokens[0], "" );
			}
		}
		sensitiveInformationTableModel = new SensitiveInformationTableModel( siph );
	}

	@Override
	protected String internalAssertResponse( MessageExchange messageExchange, SubmitContext context )
			throws AssertionException
	{
		Map<String, String> checkMap = createCheckMap( context );
		List<AssertionError> assertionErrorList = new ArrayList<AssertionError>();
		String response = messageExchange.getResponseContent();
		Set<String> messages = new HashSet<String>();

		try
		{
			for( String token : checkMap.keySet() )
			{
				boolean useRegexp = token.trim().startsWith( PREFIX );
				String description = !checkMap.get( token ).equals( "" ) ? checkMap.get( token ) : token;
				if( useRegexp )
				{
					token = token.substring( token.indexOf( PREFIX ) + 1 );
				}

				String match = SecurityScanUtil.contains( context, response, token, useRegexp );
				if( match != null )
				{
					String message = description + " - Token [" + token + "] found [" + match + "]";
					if( !messages.contains( message ) )
					{
						assertionErrorList.add( new AssertionError( message ) );
						messages.add( message );
					}
				}
			}
		}
		catch( Throwable e )
		{
			SoapUI.logError( e );
		}

		if( !messages.isEmpty() )
		{
			throw new AssertionException( assertionErrorList.toArray( new AssertionError[assertionErrorList.size()] ) );
		}

		return "OK";
	}

	//TODO check if this should be applicable to properties after all, it's not mapped for properties currently
	protected String internalAssertProperty( TestPropertyHolder source, String propertyName,
			MessageExchange messageExchange, SubmitContext context ) throws AssertionException
	{

		Map<String, String> checkMap = createCheckMap( context );
		List<AssertionError> assertionErrorList = new ArrayList<AssertionError>();
		String propertyValue = source.getPropertyValue( propertyName );
		Set<String> messages = new HashSet<String>();

		try
		{
			for( String token : checkMap.keySet() )
			{
				boolean useRegexp = token.trim().startsWith( PREFIX );
				String description = !checkMap.get( token ).equals( "" ) ? checkMap.get( token ) : token;
				if( useRegexp )
				{
					token = token.substring( token.indexOf( PREFIX ) + 1 );
				}

				String match = SecurityScanUtil.contains( context, propertyValue, token, useRegexp );
				if( match != null )
				{
					String message = description + " - Token [" + token + "] found [" + match + "] in property "
							+ propertyName;
					if( !messages.contains( message ) )
					{
						assertionErrorList.add( new AssertionError( message ) );
						messages.add( message );
					}
				}
			}
		}
		catch( Throwable e )
		{
			SoapUI.logError( e );
		}

		if( !messages.isEmpty() )
		{
			throw new AssertionException( assertionErrorList.toArray( new AssertionError[assertionErrorList.size()] ) );
		}

		return "OK";
	}

	private Map<String, String> createCheckMap( SubmitContext context )
	{
		Map<String, String> checkMap = new HashMap<String, String>();
		checkMap.putAll( createMapFromTable() );
		if( includeProjectSpecific )
		{
			checkMap.putAll( SecurityScanUtil.projectEntriesList( this ) );
		}

		if( includeGlobal )
		{
			checkMap.putAll( SecurityScanUtil.globalEntriesList() );
		}
		Map<String, String> expandedMap = propertyExpansionSupport( checkMap, context );
		return expandedMap;
	}

	private Map<String, String> propertyExpansionSupport( Map<String, String> checkMap, SubmitContext context )
	{
		Map<String, String> expanded = new HashMap<String, String>();
		for( String key : checkMap.keySet() )
		{
			expanded.put( context.expand( key ), context.expand( checkMap.get( key ) ) );
		}
		return expanded;
	}

	public static class Factory extends AbstractTestAssertionFactory
	{
		@SuppressWarnings( "unchecked" )
		public Factory()
		{
			super( SensitiveInfoExposureAssertion.ID, SensitiveInfoExposureAssertion.LABEL,
					SensitiveInfoExposureAssertion.class, new Class[] { SecurityScan.class, AbstractHttpRequest.class } );
		}

		@Override
		public String getCategory()
		{
			return AssertionCategoryMapping.SECURITY_CATEGORY;
		}

		@Override
		public Class<? extends WsdlMessageAssertion> getAssertionClassType()
		{
			return SensitiveInfoExposureAssertion.class;
		}

		@Override
		public AssertionListEntry getAssertionListEntry()
		{
			return new AssertionListEntry( SensitiveInfoExposureAssertion.ID, SensitiveInfoExposureAssertion.LABEL,
					SensitiveInfoExposureAssertion.DESCRIPTION );
		}
	}

	@Override
	protected String internalAssertRequest( MessageExchange messageExchange, SubmitContext context )
			throws AssertionException
	{
		return null;
	}

	protected XmlObject createConfiguration()
	{
		XmlObjectConfigurationBuilder builder = new XmlObjectConfigurationBuilder();
		builder.add( ASSERTION_SPECIFIC_EXPOSURE_LIST,
				assertionSpecificExposureList.toArray( new String[assertionSpecificExposureList.size()] ) );
		builder.add( INCLUDE_PROJECT_SPECIFIC, includeProjectSpecific );
		builder.add( INCLUDE_GLOBAL, includeGlobal );
		return builder.finish();
	}

	@Override
	public boolean configure()
	{
		if( dialog == null )
			buildDialog();
		if( dialog.show() )
		{
			assertionSpecificExposureList = createListFromTable();
			includeProjectSpecific = Boolean.valueOf( dialog.getFormField(
					SensitiveInformationConfigDialog.INCLUDE_PROJECT_SPECIFIC ).getValue() );
			includeGlobal = Boolean.valueOf( dialog.getFormField( SensitiveInformationConfigDialog.INCLUDE_GLOBAL )
					.getValue() );
			setConfiguration( createConfiguration() );

			return true;
		}
		return false;
	}

	private List<String> createListFromTable()
	{
		List<String> temp = new ArrayList<String>();
		for( TestProperty tp : sensitiveInformationTableModel.getHolder().getPropertyList() )
		{
			String tokenPlusDescription = tp.getName() + "###" + tp.getValue();
			temp.add( tokenPlusDescription );
		}
		return temp;
	}

	private Map<String, String> createMapFromTable()
	{
		Map<String, String> temp = new HashMap<String, String>();
		for( TestProperty tp : sensitiveInformationTableModel.getHolder().getPropertyList() )
		{
			temp.put( tp.getName(), tp.getValue() );
		}
		return temp;
	}

	protected void buildDialog()
	{
		dialog = ADialogBuilder.buildDialog( SensitiveInformationConfigDialog.class );
		dialog.setBooleanValue( SensitiveInformationConfigDialog.INCLUDE_GLOBAL, includeGlobal );
		dialog.setBooleanValue( SensitiveInformationConfigDialog.INCLUDE_PROJECT_SPECIFIC, includeProjectSpecific );
		dialog.getFormField( SensitiveInformationConfigDialog.TOKENS ).setProperty( "component", getForm() );
	}

	// TODO : update help URL
	@AForm( description = "Configure Sensitive Information Exposure Assertion", name = "Sensitive Information Exposure Assertion", helpUrl = HelpUrls.SECURITY_SENSITIVE_INFORMATION_EXPOSURE_ASSERTION_HELP )
	protected interface SensitiveInformationConfigDialog
	{

		@AField( description = "Sensitive informations to check. Use ~ as prefix for values that are regular expressions.", name = "Sensitive Information Tokens", type = AFieldType.COMPONENT )
		public final static String TOKENS = "Sensitive Information Tokens";

		@AField( description = "Include project specific sensitive information configuration", name = "Project Specific", type = AFieldType.BOOLEAN )
		public final static String INCLUDE_PROJECT_SPECIFIC = "Project Specific";

		@AField( description = "Include global sensitive information configuration", name = "Global Configuration", type = AFieldType.BOOLEAN )
		public final static String INCLUDE_GLOBAL = "Global Configuration";

	}

	@Override
	public void release()
	{
		if( dialog != null )
			dialog.release();

		super.release();
	}

	public JPanel getForm()
	{
		if( sensitiveInfoTableForm == null )
		{
			sensitiveInfoTableForm = new JPanel( new BorderLayout() );

			JXToolBar toolbar = UISupport.createToolbar();

			toolbar.add( UISupport.createToolbarButton( new AddTokenAction() ) );
			toolbar.add( UISupport.createToolbarButton( new RemoveTokenAction() ) );

			tokenTable = new JXTable( sensitiveInformationTableModel );
			tokenTable.setPreferredSize( new Dimension( 200, 100 ) );
			sensitiveInfoTableForm.add( toolbar, BorderLayout.NORTH );
			sensitiveInfoTableForm.add( new JScrollPane( tokenTable ), BorderLayout.CENTER );
		}

		return sensitiveInfoTableForm;
	}

	class AddTokenAction extends AbstractAction
	{

		public AddTokenAction()
		{
			putValue( Action.SMALL_ICON, UISupport.createImageIcon( "/add_property.gif" ) );
			putValue( Action.SHORT_DESCRIPTION, "Adds a token to assertion" );
		}

		@Override
		public void actionPerformed( ActionEvent arg0 )
		{
			String newToken = "";
			while( newToken.trim().length() == 0 )
			{
				newToken = UISupport.prompt( "Enter token", "New Token", newToken );

				if( newToken == null )
				{
					return;
				}

				if( newToken.trim().length() == 0 )
				{
					UISupport.showErrorMessage( "Enter token name!" );
				}
			}

			String newValue = "";
			newValue = UISupport.prompt( "Enter description", "New Description", newValue );

			if( newValue == null )
			{
				newValue = "";
			}

			sensitiveInformationTableModel.addToken( newToken, newValue );
		}

	}

	class RemoveTokenAction extends AbstractAction
	{

		public RemoveTokenAction()
		{
			putValue( Action.SMALL_ICON, UISupport.createImageIcon( "/remove_property.gif" ) );
			putValue( Action.SHORT_DESCRIPTION, "Removes token from assertion" );
		}

		@Override
		public void actionPerformed( ActionEvent arg0 )
		{
			sensitiveInformationTableModel.removeRows( tokenTable.getSelectedRows() );
		}

	}
}
