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