View Javadoc

1   /*
2    *  soapUI, copyright (C) 2004-2008 eviware.com 
3    *
4    *  soapUI is free software; you can redistribute it and/or modify it under the 
5    *  terms of version 2.1 of the GNU Lesser General Public License as published by 
6    *  the Free Software Foundation.
7    *
8    *  soapUI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 
9    *  even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
10   *  See the GNU Lesser General Public License for more details at gnu.org.
11   */
12  
13  package com.eviware.soapui.support.xml;
14  
15  import java.awt.Color;
16  import java.awt.Dimension;
17  import java.awt.Font;
18  import java.awt.Toolkit;
19  import java.awt.event.ActionEvent;
20  import java.awt.event.ActionListener;
21  import java.awt.event.FocusEvent;
22  import java.awt.event.FocusListener;
23  import java.lang.ref.Reference;
24  import java.lang.ref.ReferenceQueue;
25  import java.lang.ref.WeakReference;
26  import java.util.HashMap;
27  import java.util.Map;
28  
29  import javax.swing.AbstractAction;
30  import javax.swing.Action;
31  import javax.swing.BorderFactory;
32  import javax.swing.JPopupMenu;
33  import javax.swing.event.UndoableEditEvent;
34  import javax.swing.event.UndoableEditListener;
35  import javax.swing.text.BadLocationException;
36  import javax.swing.undo.CannotRedoException;
37  import javax.swing.undo.CannotUndoException;
38  import javax.swing.undo.UndoManager;
39  
40  import org.syntax.jedit.InputHandler;
41  import org.syntax.jedit.JEditTextArea;
42  import org.syntax.jedit.SyntaxStyle;
43  import org.syntax.jedit.tokenmarker.GroovyTokenMarker;
44  import org.syntax.jedit.tokenmarker.JavaScriptTokenMarker;
45  import org.syntax.jedit.tokenmarker.Token;
46  import org.syntax.jedit.tokenmarker.TokenMarker;
47  import org.syntax.jedit.tokenmarker.XMLTokenMarker;
48  
49  import com.eviware.soapui.SoapUI;
50  import com.eviware.soapui.model.settings.SettingsListener;
51  import com.eviware.soapui.settings.UISettings;
52  import com.eviware.soapui.support.UISupport;
53  import com.eviware.soapui.support.actions.FindAndReplaceDialog;
54  import com.eviware.soapui.support.actions.FindAndReplaceable;
55  import com.eviware.soapui.support.components.JEditorStatusBar.JEditorStatusBarTarget;
56  import com.eviware.soapui.support.xml.actions.FormatXmlAction;
57  import com.eviware.soapui.support.xml.actions.LoadXmlTextAreaAction;
58  import com.eviware.soapui.support.xml.actions.SaveXmlTextAreaAction;
59  
60  /***
61   * JEditTextArea extension targeted specifically at XML-editing.
62   *
63   * //@todo move font handling to subclass
64   * 
65   * @author Ole.Matzura
66   */
67  
68  public class JXEditTextArea extends JEditTextArea implements
69  		UndoableEditListener, FocusListener, FindAndReplaceable, JEditorStatusBarTarget
70  {
71  	public static final int UNDO_LIMIT = 1500;
72  	private UndoManager undoManager;
73  	private UndoAction undoAction;
74  	private RedoAction redoAction;
75  	private FindAndReplaceDialog findAndReplaceAction;
76  	private boolean discardEditsOnSet = true;
77  	private GoToLineAction goToLineAction;
78  
79  	
80  	public static JXEditTextArea createXmlEditor( boolean addPopup )
81  	{
82  		JXEditTextArea editArea = new JXEditTextArea( new XMLTokenMarker() );
83  		
84  		if( addPopup )
85  		{
86  		   JPopupMenu inputPopup = new JPopupMenu();
87  		   
88  		   inputPopup.add(new FormatXmlAction( editArea));
89  			inputPopup.addSeparator();
90  			inputPopup.add(editArea.getUndoAction());
91  			inputPopup.add(editArea.getRedoAction());
92  			inputPopup.add(editArea.createCopyAction());
93  			inputPopup.add(editArea.createCutAction());
94  			inputPopup.add(editArea.createPasteAction());
95  			inputPopup.addSeparator();
96  			inputPopup.add(editArea.getFindAndReplaceAction());
97  			inputPopup.addSeparator();
98  			inputPopup.add(editArea.getGoToLineAction());
99  			
100 			inputPopup.addSeparator(); 
101 			inputPopup.add(new SaveXmlTextAreaAction( editArea, "Save SAML Assertion" ));
102 			inputPopup.add(new LoadXmlTextAreaAction( editArea, "Load SAML Assertion" ));
103 			
104 			editArea.setRightClickPopup( inputPopup );
105 		}
106 		
107 		return editArea;
108 	}
109 	
110 	public static JXEditTextArea createGroovyEditor()
111 	{
112 		return new JXEditTextArea( new GroovyTokenMarker() );
113 	}
114 	
115 	
116 	public static JXEditTextArea createJavaScriptEditor()
117 	{
118 		return new JXEditTextArea( new JavaScriptTokenMarker() );
119 	}
120 	
121 	public JXEditTextArea( TokenMarker tokenMarker )
122 	{
123 		getPainter().setFont(UISupport.getEditorFont());
124 		getPainter().setLineHighlightColor( new Color( 240, 240, 180 ) );
125 		getPainter().setStyles(createXmlStyles());
126 		setTokenMarker(tokenMarker);
127 		setBorder(BorderFactory.createEtchedBorder());
128 		addFocusListener(this);
129 
130 		undoAction = new UndoAction();
131 		getInputHandler().addKeyBinding("C+Z", undoAction);
132 		redoAction = new RedoAction();
133 		getInputHandler().addKeyBinding("C+Y", redoAction);
134 		findAndReplaceAction = new FindAndReplaceDialog( this );
135 		getInputHandler().addKeyBinding( "C+F", findAndReplaceAction );
136 		getInputHandler().addKeyBinding( "F3", findAndReplaceAction );
137 		getInputHandler().addKeyBinding("A+RIGHT", new NextElementValueAction() );
138 		getInputHandler().addKeyBinding("A+LEFT", new PreviousElementValueAction() );
139 		getInputHandler().addKeyBinding("C+D", new DeleteLineAction() );
140 		getInputHandler().addKeyBinding( "S+INSERT", createPasteAction()  );
141 		getInputHandler().addKeyBinding( "S+DELETE", createCutAction() );
142 		
143 		goToLineAction = new GoToLineAction();
144 		getInputHandler().addKeyBinding( "CA+L", goToLineAction );
145 		
146 		setMinimumSize( new Dimension( 50, 50 ));
147 	   new InternalSettingsListener( this );
148 	}
149 	
150 	@Override
151 	public void addNotify()
152 	{
153 		super.addNotify();
154 		getDocument().addUndoableEditListener(this);
155 	}
156 	
157 	@Override
158 	public void removeNotify()
159 	{
160 		super.removeNotify();
161 		getDocument().removeUndoableEditListener(this);
162 	}
163 
164 	public Action getFindAndReplaceAction()
165 	{
166 		return findAndReplaceAction;
167 	}
168 	
169 	public Action getGoToLineAction()
170 	{
171 		return goToLineAction;
172 	}
173 
174 	public Action getRedoAction()
175 	{
176 		return redoAction;
177 	}
178 
179 	public Action getUndoAction()
180 	{
181 		return undoAction;
182 	}
183 
184 	public void setText(String text)
185 	{
186 		if( text != null && text.equals( getText() ))
187 			return;
188 			
189 		super.setText( text == null ? "" : text);
190 		
191 		if( discardEditsOnSet && undoManager != null )
192 			undoManager.discardAllEdits();
193 	}
194 	
195 	public boolean isDiscardEditsOnSet()
196 	{
197 		return discardEditsOnSet;
198 	}
199 
200 	public void setDiscardEditsOnSet(boolean discardEditsOnSet)
201 	{
202 		this.discardEditsOnSet = discardEditsOnSet;
203 	}
204 
205 	public UndoManager getUndoManager()
206 	{
207 		return undoManager;
208 	}
209 
210 	public SyntaxStyle[] createXmlStyles()
211 	{
212 		SyntaxStyle[] styles = new SyntaxStyle[Token.ID_COUNT];
213 
214 		styles[Token.COMMENT1] = new SyntaxStyle(Color.black, true, false);
215 		styles[Token.COMMENT2] = new SyntaxStyle(new Color(0x990033), true, false);
216 		styles[Token.KEYWORD1] = new SyntaxStyle(Color.blue, false, false);
217 		styles[Token.KEYWORD2] = new SyntaxStyle(Color.magenta, false, false);
218 		styles[Token.KEYWORD3] = new SyntaxStyle(new Color(0x009600), false,
219 				false);
220 		styles[Token.LITERAL1] = new SyntaxStyle(new Color(0x650099), false,
221 				false);
222 		styles[Token.LITERAL2] = new SyntaxStyle(new Color(0x650099), false, true);
223 		styles[Token.LABEL] = new SyntaxStyle(new Color(0x990033), false, true);
224 		styles[Token.OPERATOR] = new SyntaxStyle(Color.black, false, true);
225 		styles[Token.INVALID] = new SyntaxStyle(Color.red, false, true);
226 
227 		return styles;
228 	}
229 
230 	private void createUndoMananger()
231 	{
232 		undoManager = new UndoManager();
233 		undoManager.setLimit(UNDO_LIMIT);
234 	}
235 	
236 	/*
237 
238 	private void removeUndoMananger()
239 	{
240 		if (undoManager == null)
241 			return;
242 		undoManager.end();
243 		undoManager = null;
244 	}
245 	
246 	*/
247 
248 	public void focusGained(FocusEvent fe)
249 	{
250 		if (isEditable() && undoManager == null )
251 			createUndoMananger();
252 	}
253 	
254 	public void setEnabledAndEditable( boolean flag )
255 	{
256 		super.setEnabled( flag );
257 		setEditable( flag );
258 	}
259 
260 	public void setEditable(boolean enabled)
261 	{
262 		super.setEditable(enabled);
263 		setCaretVisible( enabled );
264 		getPainter().setLineHighlightEnabled( enabled );
265 		
266 		//getPainter().setBackground( enabled ? Color.WHITE : new Color(238, 238, 238) );
267 		repaint();
268 	}
269 
270 	public void focusLost(FocusEvent fe)
271 	{
272 		//removeUndoMananger();
273 	}
274 
275 	public void undoableEditHappened(UndoableEditEvent e)
276 	{
277 		if (undoManager != null)
278 			undoManager.addEdit(e.getEdit());
279 	}
280 
281 	private static ReferenceQueue<JXEditTextArea> testQueue = new ReferenceQueue<JXEditTextArea>();
282 	private static Map<WeakReference<JXEditTextArea>, InternalSettingsListener> testMap = 
283 		new HashMap<WeakReference<JXEditTextArea>, InternalSettingsListener>();
284 	
285 	static
286 	{
287 		new Thread( new Runnable() {
288 
289 			public void run()
290 			{
291 				while( true )
292 				{
293 //					System.out.println( "Waiting for weak references to be released.." );
294 					
295 					try
296 					{
297 						Reference<? extends JXEditTextArea> ref = testQueue.remove();
298 //						System.out.println( "Got ref to clear" );
299 						InternalSettingsListener listener = testMap.remove( ref );
300 						if( listener != null )
301 						{
302 //							System.out.println( "Releasing listener" );
303 							listener.release();
304 						}
305 						else
306 						{
307 //							System.out.println( "Listener not found" );
308 						}
309 					}
310 					catch( InterruptedException e )
311 					{
312 						SoapUI.logError( e );
313 					}
314 				}
315 				
316 			}}, "ReferenceQueueMonitor" ).start();
317 	}
318 	
319 	private static final class InternalSettingsListener implements SettingsListener
320 	{
321 		private WeakReference<JXEditTextArea> textArea;
322 		
323 		public InternalSettingsListener( JXEditTextArea area )
324 		{
325 //			System.out.println( "Creating weakreference for textarea" );
326 			textArea = new WeakReference<JXEditTextArea>(area,testQueue);
327 			testMap.put( textArea, this );
328 			SoapUI.getSettings().addSettingsListener(this);
329 		}
330 
331 		public void release()
332 		{
333 			if( textArea.get() == null )
334 			{
335 				SoapUI.getSettings().removeSettingsListener(this);
336 			}
337 			else
338 			{
339 				System.err.println( "Error, cannot release listener" );
340 			}
341 		}
342 
343 		public void settingChanged(String name, String newValue, String oldValue)
344 		{
345 			if( name.equals( UISettings.EDITOR_FONT ) && textArea.get() != null)
346 			{
347 				textArea.get().getPainter().setFont( Font.decode( newValue ));
348 				textArea.get().invalidate();
349 			}
350 		}
351 		
352 		
353 	}
354 
355 	private class UndoAction extends AbstractAction
356 	{
357 		public UndoAction()
358 		{
359 			super( "Undo");
360 			putValue( Action.ACCELERATOR_KEY, UISupport.getKeyStroke( "menu Z" ));
361 		}
362 		
363 		public void actionPerformed(ActionEvent e)
364 		{
365 			undo();
366 		}
367 	}
368 
369 	private class RedoAction extends AbstractAction
370 	{
371 		public RedoAction()
372 		{
373 			super( "Redo");
374 			putValue( Action.ACCELERATOR_KEY, UISupport.getKeyStroke( "menu Y" ));
375 		}
376 
377 		public void actionPerformed(ActionEvent e)
378 		{
379 			redo();
380 		}
381 	}
382 	
383 	private final class GoToLineAction extends AbstractAction
384 	{
385 		public GoToLineAction()
386 		{
387 			super( "Go To Line" );
388 			putValue( Action.SHORT_DESCRIPTION, "Moves the caret to the specified line" );
389 			putValue( Action.ACCELERATOR_KEY, UISupport.getKeyStroke( "menu alt L" ));
390 		}
391 		
392 		public void actionPerformed( ActionEvent e )
393 		{
394 			String line = UISupport.prompt( "Enter line-number to (1.." + (getLineCount()) + ")", "Go To Line", 
395 						String.valueOf( getCaretLine()+1 ));
396 			
397 			if( line != null )
398 			{
399 				try
400 				{
401 					int ln = Integer.parseInt( line )-1;
402 					if( ln >= 0 && ln < getLineCount() )
403 					{
404 						scrollTo( ln, 0 );
405 						setCaretPosition( getLineStartOffset( ln ));
406 					}
407 				}
408 				catch( NumberFormatException e1 )
409 				{
410 				}
411 			}
412 		}
413 	}
414 	
415 	public class NextElementValueAction implements ActionListener
416 	{
417 		public void actionPerformed(ActionEvent e)
418 		{
419 			toNextElement();
420 		}
421 	}
422 	
423 	public class PreviousElementValueAction implements ActionListener
424 	{
425 		public void actionPerformed(ActionEvent e)
426 		{
427 			toPreviousElement();
428 		}
429 	}
430 
431 	
432 	public class DeleteLineAction implements ActionListener
433 	{
434 		public void actionPerformed(ActionEvent e)
435 		{
436 			if( !isEditable() )
437 			{
438 				getToolkit().beep();
439 				return;
440 			}
441 			
442 			int caretLine = getCaretLine();
443 			if( caretLine == -1 ) return;
444 			int lineStartOffset = getLineStartOffset( caretLine);
445 			int lineEndOffset = getLineEndOffset( caretLine);
446 			
447 			try
448 			{
449 				int len = lineEndOffset-lineStartOffset;
450 				if( lineStartOffset+len >= getDocumentLength() ) len = getDocumentLength()-lineStartOffset;
451 				
452 				getDocument().remove( lineStartOffset, len );
453 			}
454 			catch (BadLocationException e1)
455 			{
456 				SoapUI.logError( e1 );
457 			}
458 		}
459 	}
460 
461 	public Action createCopyAction()
462 	{
463 		return new InputHandler.clip_copy();
464 	}
465 	
466 	public void undo()
467 	{
468 		if( !isEditable()  )
469 		{
470 			getToolkit().beep();
471 			return;
472 		}
473 		
474 		try
475 		{
476 			if( undoManager != null )
477 				undoManager.undo();
478 		}
479 		catch (CannotUndoException cue)
480 		{
481 			Toolkit.getDefaultToolkit().beep();
482 		}
483 	}
484 
485 	public Action createCutAction()
486 	{
487 		return new InputHandler.clip_cut();
488 	}
489 	
490 	public Action createPasteAction()
491 	{
492 		return new InputHandler.clip_paste();
493 	}
494 
495 	public int getCaretColumn()
496 	{
497 		int pos = getCaretPosition();
498 		int line = getLineOfOffset( pos );
499 		
500 		return pos - getLineStartOffset( line );
501 	}
502 
503 	public void toNextElement()
504 	{
505 		int pos = getCaretPosition();
506 		String text = getText();
507 		
508 		while( pos < text.length() )
509 		{
510 			// find ending >
511 			if( text.charAt( pos ) == '>' && pos < text.length()-1 && 
512 					(pos > 2 && !text.substring(pos-2,pos).equals( "--" )) &&
513 					(pos > 1 && text.charAt(pos-1) != '/' ) &&
514 					text.indexOf( '/', pos ) == text.indexOf( '<', pos) +1 &&
515 					text.lastIndexOf( '/', pos ) != text.lastIndexOf( '<', pos) +1	)
516 			{
517 				setCaretPosition( pos+1 );
518 				return;
519 			}		
520 			
521 			pos++;
522 		}			
523 		
524 		getToolkit().beep();
525 	}
526 
527 	public void toPreviousElement()
528 	{
529 		int pos = getCaretPosition()-2;
530 		String text = getText();
531 		
532 		while( pos > 0 )
533 		{
534 			// find ending >
535 			if( text.charAt( pos ) == '>' && pos < text.length()-1 && 
536 					(pos > 2 && !text.substring(pos-2,pos).equals( "--" )) &&
537 					(pos > 1 && text.charAt(pos-1) != '/' ) &&
538 					text.indexOf( '/', pos ) == text.indexOf( '<', pos) +1 &&
539 					text.lastIndexOf( '/', pos ) != text.lastIndexOf( '<', pos) +1	)
540 			{
541 				setCaretPosition( pos+1 );
542 				return;
543 			}		
544 			
545 			pos--;
546 		}				
547 
548 		getToolkit().beep();
549 	}
550 
551 	public boolean canUndo()
552 	{
553 		return undoManager != null && undoManager.canUndo();
554 	}
555 	
556 	public boolean canRedo()
557 	{
558 		return undoManager != null && undoManager.canRedo();
559 	}
560 
561 	public void redo()
562 	{
563 		if( !isEditable()  )
564 		{
565 			getToolkit().beep();
566 			return;
567 		}
568 		
569 		try
570 		{
571 			if( canRedo() )
572 				undoManager.redo();
573 		}
574 		catch (CannotRedoException cue)
575 		{
576 			Toolkit.getDefaultToolkit().beep();
577 		}	}
578 }