content: /*******************************************************************************
   * Copyright (c) 2000, 2009 IBM Corporation and others.
   * All rights reserved. This program and the accompanying materials
   * are made available under the terms of the Eclipse Public License v1.0
   * which accompanies this distribution, and is available at
   * http://www.eclipse.org/legal/epl-v10.html
   *
   * Contributors:
   *     IBM Corporation - initial API and implementation
   *******************************************************************************/
  package org.eclipse.wst.jsdt.internal.ui.javaeditor;
  
  import java.io.ByteArrayInputStream;
  import java.io.ByteArrayOutputStream;
  import java.io.DataInputStream;
  import java.io.DataOutputStream;
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.HashSet;
  import java.util.List;
  import java.util.ResourceBundle;
  
  import org.eclipse.core.runtime.Assert;
  import org.eclipse.core.runtime.CoreException;
  import org.eclipse.jface.text.IRewriteTarget;
  import org.eclipse.jface.text.ITextOperationTarget;
  import org.eclipse.jface.text.ITextSelection;
  import org.eclipse.jface.text.Region;
  import org.eclipse.jface.viewers.ISelection;
  import org.eclipse.swt.SWTError;
  import org.eclipse.swt.custom.BusyIndicator;
  import org.eclipse.swt.dnd.ByteArrayTransfer;
  import org.eclipse.swt.dnd.Clipboard;
  import org.eclipse.swt.dnd.DND;
  import org.eclipse.swt.dnd.RTFTransfer;
  import org.eclipse.swt.dnd.TextTransfer;
  import org.eclipse.swt.dnd.Transfer;
  import org.eclipse.swt.dnd.TransferData;
  import org.eclipse.swt.widgets.Display;
  import org.eclipse.swt.widgets.Shell;
  import org.eclipse.ui.IWorkbenchPartSite;
  import org.eclipse.ui.texteditor.IAbstractTextEditorHelpContextIds;
  import org.eclipse.ui.texteditor.ITextEditor;
  import org.eclipse.ui.texteditor.IWorkbenchActionDefinitionIds;
  import org.eclipse.ui.texteditor.TextEditorAction;
  import org.eclipse.wst.jsdt.core.IJavaScriptUnit;
  import org.eclipse.wst.jsdt.core.IJavaScriptElement;
  import org.eclipse.wst.jsdt.core.Signature;
  import org.eclipse.wst.jsdt.core.dom.ASTNode;
  import org.eclipse.wst.jsdt.core.dom.JavaScriptUnit;
  import org.eclipse.wst.jsdt.core.dom.IBinding;
  import org.eclipse.wst.jsdt.core.dom.ITypeBinding;
  import org.eclipse.wst.jsdt.core.dom.Name;
  import org.eclipse.wst.jsdt.core.dom.rewrite.ImportRewrite;
  import org.eclipse.wst.jsdt.internal.corext.codemanipulation.ImportReferencesCollector;
  import org.eclipse.wst.jsdt.internal.corext.codemanipulation.StubUtility;
  import org.eclipse.wst.jsdt.internal.corext.dom.Bindings;
  import org.eclipse.wst.jsdt.internal.corext.util.JavaModelUtil;
  import org.eclipse.wst.jsdt.internal.ui.JavaScriptPlugin;
  import org.eclipse.wst.jsdt.ui.PreferenceConstants;
  
  
  /**
   * Action for cut/copy and paste with support for adding imports on paste.
   */
  public final class ClipboardOperationAction extends TextEditorAction {
  
  	public static class ClipboardData {
  		private String fOriginHandle;
  		private String[] fTypeImports;
  		private String[] fStaticImports;
  
  		public ClipboardData(IJavaScriptElement origin, String[] typeImports, String[] staticImports) {
  			Assert.isNotNull(origin);
  			Assert.isNotNull(typeImports);
  			Assert.isNotNull(staticImports);
  
  			fTypeImports= typeImports;
  			fStaticImports= staticImports;
  			fOriginHandle= origin.getHandleIdentifier();
  		}
  
  		public ClipboardData(byte[] bytes) throws IOException {
  			DataInputStream dataIn = new DataInputStream(new ByteArrayInputStream(bytes));
  			try {
  				fOriginHandle= dataIn.readUTF();
  				fTypeImports= readArray(dataIn);
  				fStaticImports= readArray(dataIn);
  			} finally {
  				dataIn.close();
  			}
  		}
  
  		private static String[] readArray(DataInputStream dataIn) throws IOException {
  			int count= dataIn.readInt();
  
  			String[] array= new String[count];
  			for (int i = 0; i < count; i++) {
  				array[i]= dataIn.readUTF();
  			}
  			return array;
  		}
  
  		private static void writeArray(DataOutputStream dataOut, String[] array) throws IOException {
  			dataOut.writeInt(array.length);
  			for (int i = 0; i < array.length; i++) {
  				dataOut.writeUTF(array[i]);
  			}
  		}
  
  		public String[] getTypeImports() {
  			return fTypeImports;
  		}
  
  		public String[] getStaticImports() {
  			return fStaticImports;
  		}
  
  		public boolean isFromSame(IJavaScriptElement elem) {
  			return fOriginHandle.equals(elem.getHandleIdentifier());
  		}
  
  		public byte[] serialize() throws IOException {
  			ByteArrayOutputStream out = new ByteArrayOutputStream();
  			DataOutputStream dataOut = new DataOutputStream(out);
  			try {
  				dataOut.writeUTF(fOriginHandle);
  				writeArray(dataOut, fTypeImports);
  				writeArray(dataOut, fStaticImports);
  			} finally {
  				dataOut.close();
  				out.close();
  			}
  
  			return out.toByteArray();
  		}
  	}
  
  
  	private static class ClipboardTransfer extends ByteArrayTransfer {
  
  		private static final String TYPE_NAME = "source-with-imports-transfer-format" + System.currentTimeMillis(); //$NON-NLS-1$
  
  		private static final int TYPEID = registerType(TYPE_NAME);
  
  		/* (non-Javadoc)
  		 * @see org.eclipse.swt.dnd.Transfer#getTypeIds()
  		 */
  		protected int[] getTypeIds() {
  			return new int[] { TYPEID };
  		}
  
  		/* (non-Javadoc)
  		 * @see org.eclipse.swt.dnd.Transfer#getTypeNames()
  		 */
  		protected String[] getTypeNames() {
  			return new String[] { TYPE_NAME };
  		}
  
  		/* (non-Javadoc)
  		 * @see org.eclipse.swt.dnd.Transfer#javaToNative(java.lang.Object, org.eclipse.swt.dnd.TransferData)
  		 */
  		protected void javaToNative(Object data, TransferData transferData) {
  			if (data instanceof ClipboardData) {
  				try {
  					super.javaToNative(((ClipboardData) data).serialize(), transferData);
  				} catch (IOException e) {
  					//it's best to send nothing if there were problems
  				}
  			}
  		}
  
  		/* (non-Javadoc)
  		 * Method declared on Transfer.
  		 */
  		protected Object nativeToJava(TransferData transferData) {
  			byte[] bytes = (byte[]) super.nativeToJava(transferData);
  			if (bytes != null) {
  				try {
  					return new ClipboardData(bytes);
  				} catch (IOException e) {
  				}
  			}
  			return null;
  		}
  
  	}
  
  	private static final ClipboardTransfer fgTransferInstance = new ClipboardTransfer();
  
  	/** The text operation code */
  	private int fOperationCode= -1;
  	/** The text operation target */
  	private ITextOperationTarget fOperationTarget;
  
  
  	/**
  	 * Creates the action.
  	 */
  	public ClipboardOperationAction(ResourceBundle bundle, String prefix, ITextEditor editor, int operationCode) {
  		super(bundle, prefix, editor);
  		fOperationCode= operationCode;
  
  		if (operationCode == ITextOperationTarget.CUT) {
  			setHelpContextId(IAbstractTextEditorHelpContextIds.CUT_ACTION);
  			setActionDefinitionId(IWorkbenchActionDefinitionIds.CUT);
  		} else if (operationCode == ITextOperationTarget.COPY) {
  			setHelpContextId(IAbstractTextEditorHelpContextIds.COPY_ACTION);
  			setActionDefinitionId(IWorkbenchActionDefinitionIds.COPY);
  		} else if (operationCode == ITextOperationTarget.PASTE) {
  			setHelpContextId(IAbstractTextEditorHelpContextIds.PASTE_ACTION);
  			setActionDefinitionId(IWorkbenchActionDefinitionIds.PASTE);
  		} else {
  			Assert.isTrue(false, "Invalid operation code"); //$NON-NLS-1$
  		}
  		update();
  	}
  
  	private boolean isReadOnlyOperation() {
  		return fOperationCode == ITextOperationTarget.COPY;
  	}
  
  
  	/* (non-Javadoc)
  	 * @see org.eclipse.jface.action.IAction#run()
  	 */
  	public void run() {
  		if (fOperationCode == -1 || fOperationTarget == null)
  			return;
  
  		ITextEditor editor= getTextEditor();
  		if (editor == null)
  			return;
  
  		if (!isReadOnlyOperation() && !validateEditorInputState())
  			return;
  
  		BusyIndicator.showWhile(getDisplay(), new Runnable() {
  			public void run() {
  				internalDoOperation();
  			}
  		});
  	}
  
  	private Shell getShell() {
  		ITextEditor editor= getTextEditor();
  		if (editor != null) {
  			IWorkbenchPartSite site= editor.getSite();
  			Shell shell= site.getShell();
  			if (shell != null && !shell.isDisposed()) {
  				return shell;
  			}
  		}
  		return null;
  	}
  
  	private Display getDisplay() {
  		Shell shell= getShell();
  		if (shell != null) {
  			return shell.getDisplay();
  		}
  		return null;
  	}
  
  
  	protected final void internalDoOperation() {
  		if (PreferenceConstants.getPreferenceStore().getBoolean(PreferenceConstants.EDITOR_IMPORTS_ON_PASTE)) {
  			if (fOperationCode == ITextOperationTarget.PASTE) {
  				doPasteWithImportsOperation();
  			} else {
  				doCutCopyWithImportsOperation();
  			}
  		} else {
  			fOperationTarget.doOperation(fOperationCode);
  		}
  	}
  
  	/* (non-Javadoc)
  	 * @see org.eclipse.ui.texteditor.IUpdate#update()
  	 */
  	public void update() {
  		super.update();
  
  		if (!isReadOnlyOperation() && !canModifyEditor()) {
  			setEnabled(false);
  			return;
  		}
  
  		ITextEditor editor= getTextEditor();
  		if (fOperationTarget == null && editor!= null && fOperationCode != -1)
  			fOperationTarget= (ITextOperationTarget) editor.getAdapter(ITextOperationTarget.class);
  
  		boolean isEnabled= (fOperationTarget != null && fOperationTarget.canDoOperation(fOperationCode));
  		setEnabled(isEnabled);
  	}
  
  	/* (non-Javadoc)
  	 * @see org.eclipse.ui.texteditor.TextEditorAction#setEditor(org.eclipse.ui.texteditor.ITextEditor)
  	 */
  	public void setEditor(ITextEditor editor) {
  		super.setEditor(editor);
  		fOperationTarget= null;
  	}
  
  
  	private void doCutCopyWithImportsOperation() {
  		ITextEditor editor= getTextEditor();
  		IJavaScriptElement inputElement= (IJavaScriptElement) editor.getEditorInput().getAdapter(IJavaScriptElement.class);
  		ISelection selection= editor.getSelectionProvider().getSelection();
  
  		Object clipboardData= null;
  		if (inputElement != null && selection instanceof ITextSelection && !selection.isEmpty()) {
  			ITextSelection textSelection= (ITextSelection) selection;
  			if (isNonTrivialSelection(textSelection)) {
  				clipboardData= getClipboardData(inputElement, textSelection.getOffset(), textSelection.getLength());
  			}
  		}
  
  		fOperationTarget.doOperation(fOperationCode);
  
  		if (clipboardData != null) {
  			Clipboard clipboard= new Clipboard(getDisplay());
  			try {
  				/*
  				 * We currently make assumptions about what the styled text widget sets,
  				 * see https://bugs.eclipse.org/bugs/show_bug.cgi?id=61876
  				 */
  				Object textData= clipboard.getContents(TextTransfer.getInstance());
  				Object rtfData= clipboard.getContents(RTFTransfer.getInstance());
  
  				ArrayList datas= new ArrayList(3);
  				ArrayList transfers= new ArrayList(3);
  				if (textData != null) {
  					datas.add(textData);
  					transfers.add(TextTransfer.getInstance());
  				}
  				if (rtfData != null) {
  					datas.add(rtfData);
  					transfers.add(RTFTransfer.getInstance());
  				}
  
  				/*
  				 * Don't add if we didn't get any data from the clipboard
  				 * see: https://bugs.eclipse.org/bugs/show_bug.cgi?id=70077
  				 */
  				if (datas.isEmpty())
  					return;
  
  				datas.add(clipboardData);
  				transfers.add(fgTransferInstance);
  
  				Transfer[] dataTypes= (Transfer[]) transfers.toArray(new Transfer[transfers.size()]);
  				Object[] data= datas.toArray();
  				setClipboardContents(clipboard, data, dataTypes);
  			} finally {
  				clipboard.dispose();
  			}
  		}
  	}
  
  	private void setClipboardContents(Clipboard clipboard, Object[] datas, Transfer[] transfers) {
  		try {
  			clipboard.setContents(datas, transfers);
  		} catch (SWTError e) {
  			if (e.code != DND.ERROR_CANNOT_SET_CLIPBOARD) {
  				throw e;
  			}
  			// silently fail.  see e.g. https://bugs.eclipse.org/bugs/show_bug.cgi?id=65975
  		}
  	}
  
  	private boolean isNonTrivialSelection(ITextSelection selection) {
  		if (selection.getLength() < 30) {
  			String text= selection.getText();
  			if (text != null) {
  				for (int i= 0; i < text.length(); i++) {
  					if (!Character.isJavaIdentifierPart(text.charAt(i))) {
  						return true;
  					}
  				}
  			}
  			return false;
  		}
  		return true;
  	}
  
  
  	private ClipboardData getClipboardData(IJavaScriptElement inputElement, int offset, int length) {
  		JavaScriptUnit astRoot= JavaScriptPlugin.getDefault().getASTProvider().getAST(inputElement, ASTProvider.WAIT_ACTIVE_ONLY, null);
  		if (astRoot == null) {
  			return null;
  		}
  
  		// do process import if selection spans over import declaration or package
  		List list= astRoot.imports();
  		if (!list.isEmpty()) {
  			if (offset < ((ASTNode) list.get(list.size() - 1)).getStartPosition()) {
  				return null;
  			}
  		} else if (astRoot.getPackage() != null) {
  			if (offset < ((ASTNode) astRoot.getPackage()).getStartPosition()) {
  				return null;
  			}
  		}
  
  		ArrayList typeImportsRefs= new ArrayList();
  		ArrayList staticImportsRefs= new ArrayList();
  
  		ImportReferencesCollector.collect(astRoot, inputElement.getJavaScriptProject(), new Region(offset, length), typeImportsRefs, staticImportsRefs);
  
  		if (typeImportsRefs.isEmpty() && staticImportsRefs.isEmpty()) {
  			return null;
  		}
  
  		HashSet namesToImport= new HashSet(typeImportsRefs.size());
  		for (int i= 0; i < typeImportsRefs.size(); i++) {
  			Name curr= (Name) typeImportsRefs.get(i);
  			IBinding binding= curr.resolveBinding();
  			if (binding != null && binding.getKind() == IBinding.TYPE) {
  				ITypeBinding typeBinding= (ITypeBinding) binding;
  				if (typeBinding.isArray()) {
  					typeBinding= typeBinding.getElementType();
  				}
  				
  				if (typeBinding.isMember() || typeBinding.isTopLevel()) {
  					String name= Bindings.getRawQualifiedName(typeBinding);
  					if (name.length() > 0) {
  						namesToImport.add(name);
  					}
  				}
  			}
  		}
  
  		HashSet staticsToImport= new HashSet(staticImportsRefs.size());
  		for (int i= 0; i < staticImportsRefs.size(); i++) {
  			Name curr= (Name) staticImportsRefs.get(i);
  			IBinding binding= curr.resolveBinding();
  			if (binding != null) {
  				StringBuffer buf= new StringBuffer(Bindings.getImportName(binding));
  				if (binding.getKind() == IBinding.METHOD) {
  					buf.append("()"); //$NON-NLS-1$
  				}
  				staticsToImport.add(buf.toString());
  			}
  		}
  
  
  		if (namesToImport.isEmpty() && staticsToImport.isEmpty()) {
  			return null;
  		}
  
  		String[] typeImports= (String[]) namesToImport.toArray(new String[namesToImport.size()]);
  		String[] staticImports= (String[]) staticsToImport.toArray(new String[staticsToImport.size()]);
  		return new ClipboardData(inputElement, typeImports, staticImports);
  	}
  
  
  	private void doPasteWithImportsOperation() {
  		ITextEditor editor= getTextEditor();
  		IJavaScriptElement inputElement= (IJavaScriptElement) editor.getEditorInput().getAdapter(IJavaScriptElement.class);
  
  		Clipboard clipboard= new Clipboard(getDisplay());
  		ClipboardData importsData= (ClipboardData) clipboard.getContents(fgTransferInstance);
  		if (importsData != null && inputElement instanceof IJavaScriptUnit && !importsData.isFromSame(inputElement)) {
  			// combine operation and adding of imports
  			IRewriteTarget target= editor != null ? (IRewriteTarget) editor.getAdapter(IRewriteTarget.class) : null;
  			if (target != null) {
  				target.beginCompoundChange();
  			}
  			try {
  				fOperationTarget.doOperation(fOperationCode);
  				addImports((IJavaScriptUnit) inputElement, importsData);
  			} catch (CoreException e) {
  				JavaScriptPlugin.log(e);
  			} finally {
  				if (target != null) {
  					target.endCompoundChange();
  				}
  			}
  		} else {
  			fOperationTarget.doOperation(fOperationCode);
  		}
  	}
  
  
  	private void addImports(IJavaScriptUnit unit, ClipboardData data) throws CoreException {
  		ImportRewrite rewrite= StubUtility.createImportRewrite(unit, true);
  		String[] imports= data.getTypeImports();
  		for (int i= 0; i < imports.length; i++) {
  			rewrite.addImport(imports[i]);
  		}
  		String[] staticImports= data.getStaticImports();
  		for (int i= 0; i < staticImports.length; i++) {
  			String name= Signature.getSimpleName(staticImports[i]);
  			boolean isField= !name.endsWith("()"); //$NON-NLS-1$
  			if (!isField) {
  				name= name.substring(0, name.length() - 2);
  			}
  			String qualifier= Signature.getQualifier(staticImports[i]);
  			rewrite.addStaticImport(qualifier, name, isField);
  		}
  
  		JavaModelUtil.applyEdit(unit, rewrite.rewriteImports(null), false, null);
  	}
  
  
  }
  