View Javadoc

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