1
2
3
4
5
6
7
8
9
10
11
12
13 package com.eviware.soapui.impl.wsdl.teststeps.assertions.basic;
14
15 import com.eviware.soapui.SoapUI;
16 import com.eviware.soapui.config.TestAssertionConfig;
17 import com.eviware.soapui.impl.support.actions.ShowOnlineHelpAction;
18 import com.eviware.soapui.impl.wsdl.WsdlInterface;
19 import com.eviware.soapui.impl.wsdl.support.HelpUrls;
20 import com.eviware.soapui.impl.wsdl.support.assertions.AssertedXPathImpl;
21 import com.eviware.soapui.impl.wsdl.support.assertions.AssertedXPathsContainer;
22 import com.eviware.soapui.impl.wsdl.testcase.WsdlTestRunContext;
23 import com.eviware.soapui.impl.wsdl.teststeps.WsdlMessageAssertion;
24 import com.eviware.soapui.impl.wsdl.teststeps.WsdlTestRequestStep;
25 import com.eviware.soapui.impl.wsdl.teststeps.assertions.AbstractTestAssertionFactory;
26 import com.eviware.soapui.model.TestModelItem;
27 import com.eviware.soapui.model.iface.MessageExchange;
28 import com.eviware.soapui.model.iface.SubmitContext;
29 import com.eviware.soapui.model.propertyexpansion.PropertyExpansion;
30 import com.eviware.soapui.model.propertyexpansion.PropertyExpansionUtils;
31 import com.eviware.soapui.model.support.XPathReference;
32 import com.eviware.soapui.model.support.XPathReferenceContainer;
33 import com.eviware.soapui.model.support.XPathReferenceImpl;
34 import com.eviware.soapui.model.testsuite.*;
35 import com.eviware.soapui.model.testsuite.AssertionError;
36 import com.eviware.soapui.support.StringUtils;
37 import com.eviware.soapui.support.UISupport;
38 import com.eviware.soapui.support.components.JUndoableTextArea;
39 import com.eviware.soapui.support.components.JXToolBar;
40 import com.eviware.soapui.support.types.StringList;
41 import com.eviware.soapui.support.xml.XmlObjectConfigurationBuilder;
42 import com.eviware.soapui.support.xml.XmlObjectConfigurationReader;
43 import com.eviware.soapui.support.xml.XmlUtils;
44 import com.jgoodies.forms.builder.ButtonBarBuilder;
45 import junit.framework.ComparisonFailure;
46 import org.apache.log4j.Logger;
47 import org.apache.xmlbeans.*;
48 import org.custommonkey.xmlunit.*;
49 import org.w3c.dom.Attr;
50 import org.w3c.dom.Element;
51 import org.w3c.dom.Node;
52
53 import javax.swing.*;
54 import java.awt.*;
55 import java.awt.event.ActionEvent;
56 import java.awt.event.WindowAdapter;
57 import java.awt.event.WindowEvent;
58 import java.util.ArrayList;
59 import java.util.List;
60
61 /***
62 * Assertion that matches a specified XPath expression and its expected result
63 * against the associated WsdlTestRequests response message
64 *
65 * @author Ole.Matzura
66 */
67
68 public class XPathContainsAssertion extends WsdlMessageAssertion implements RequestAssertion, ResponseAssertion, XPathReferenceContainer
69 {
70 private final static Logger log = Logger.getLogger( XPathContainsAssertion.class );
71 private String expectedContent;
72 private String path;
73 private JDialog configurationDialog;
74 private JTextArea pathArea;
75 private JTextArea contentArea;
76 private boolean configureResult;
77 private boolean allowWildcards;
78 private boolean ignoreNamspaceDifferences;
79
80 public static final String ID = "XPath Match";
81 public static final String LABEL = "XPath Match";
82 private JCheckBox allowWildcardsCheckBox;
83 private JCheckBox ignoreNamspaceDifferencesCheckBox;
84
85 public XPathContainsAssertion( TestAssertionConfig assertionConfig, Assertable assertable )
86 {
87 super( assertionConfig, assertable, true, true, true, true );
88
89 XmlObjectConfigurationReader reader = new XmlObjectConfigurationReader( getConfiguration() );
90 path = reader.readString( "path", null );
91 expectedContent = reader.readString( "content", null );
92 allowWildcards = reader.readBoolean( "allowWildcards", false );
93 ignoreNamspaceDifferences = reader.readBoolean( "ignoreNamspaceDifferences", false );
94 }
95
96 public String getExpectedContent()
97 {
98 return expectedContent;
99 }
100
101 public void setExpectedContent( String expectedContent )
102 {
103 this.expectedContent = expectedContent;
104 setConfiguration( createConfiguration() );
105 }
106
107 /***
108 * @deprecated
109 */
110
111 @Deprecated
112 public void setContent( String content )
113 {
114 setExpectedContent( content );
115 }
116
117 public String getPath()
118 {
119 return path;
120 }
121
122 public void setPath( String path )
123 {
124 this.path = path;
125 setConfiguration( createConfiguration() );
126 }
127
128 public boolean isAllowWildcards()
129 {
130 return allowWildcards;
131 }
132
133 public void setAllowWildcards( boolean allowWildcards )
134 {
135 this.allowWildcards = allowWildcards;
136 }
137
138 public boolean isIgnoreNamspaceDifferences()
139 {
140 return ignoreNamspaceDifferences;
141 }
142
143 public void setIgnoreNamspaceDifferences( boolean ignoreNamspaceDifferences )
144 {
145 this.ignoreNamspaceDifferences = ignoreNamspaceDifferences;
146 }
147
148 @Override
149 protected String internalAssertResponse( MessageExchange messageExchange, SubmitContext context )
150 throws AssertionException
151 {
152 if( !messageExchange.hasResponse() )
153 return "Missing Response";
154 else
155 return assertContent( messageExchange.getResponseContentAsXml(), context, "Response" );
156 }
157
158 public String assertContent( String response, SubmitContext context, String type ) throws AssertionException
159 {
160 try
161 {
162 if( path == null )
163 return "Missing path for XPath assertion";
164 if( expectedContent == null )
165 return "Missing content for XPath assertion";
166
167 XmlObject xml = XmlObject.Factory.parse( response );
168 String expandedPath = PropertyExpansionUtils.expandProperties( context, path );
169 XmlObject[] items = xml.selectPath( expandedPath );
170 AssertedXPathsContainer assertedXPathsContainer = (AssertedXPathsContainer)
171 context.getProperty( AssertedXPathsContainer.ASSERTEDXPATHSCONTAINER_PROPERTY );
172
173 XmlObject contentObj = null;
174 String expandedContent = PropertyExpansionUtils.expandProperties( context, expectedContent );
175
176
177
178 if( !expandedPath.endsWith( "text()" ) )
179 {
180 try
181 {
182 contentObj = XmlObject.Factory.parse( expandedContent );
183 }
184 catch( Exception e )
185 {
186
187
188
189 }
190 }
191
192 if( items.length == 0 )
193 throw new Exception( "Missing content for xpath [" + path + "] in " + type );
194
195 XmlOptions options = new XmlOptions();
196 options.setSavePrettyPrint();
197 options.setSaveOuter();
198
199 for( int c = 0; c < items.length; c++ )
200 {
201 try
202 {
203 AssertedXPathImpl assertedXPathImpl = null;
204 if( assertedXPathsContainer != null )
205 {
206 String xpath = XmlUtils.createAbsoluteXPath( items[c].getDomNode() );
207 if( xpath != null )
208 {
209 XmlObject xmlObj = items[c];
210
211 assertedXPathImpl = new AssertedXPathImpl( this, xpath, xmlObj );
212 assertedXPathsContainer.addAssertedXPath( assertedXPathImpl );
213 }
214 }
215
216 if( contentObj == null )
217 {
218 if( items[c] instanceof XmlAnySimpleType && !( items[c] instanceof XmlQName ) )
219 {
220 String value = ( (XmlAnySimpleType) items[c] ).getStringValue();
221 String expandedValue = PropertyExpansionUtils.expandProperties( context, value );
222 XMLAssert.assertEquals( expandedContent, expandedValue );
223 }
224 else
225 {
226 Node domNode = items[c].getDomNode();
227 if( domNode.getNodeType() == Node.ELEMENT_NODE )
228 {
229 String expandedValue = PropertyExpansionUtils.expandProperties( context, XmlUtils
230 .getElementText( (Element) domNode ) );
231 XMLAssert.assertEquals( expandedContent, expandedValue );
232 }
233 else
234 {
235 String expandedValue = PropertyExpansionUtils.expandProperties( context, domNode
236 .getNodeValue() );
237 XMLAssert.assertEquals( expandedContent, expandedValue );
238 }
239 }
240 }
241 else
242 {
243 compareValues( contentObj.xmlText( options ), items[c].xmlText( options ), items[c] );
244 }
245
246 break;
247 }
248 catch( Throwable e )
249 {
250 if( c == items.length - 1 )
251 throw e;
252 }
253 }
254 }
255 catch( Throwable e )
256 {
257 String msg = "";
258
259 if( e instanceof ComparisonFailure )
260 {
261 ComparisonFailure cf = (ComparisonFailure) e;
262 String expected = cf.getExpected();
263 String actual = cf.getActual();
264
265
266
267
268
269
270
271 msg = "XPathContains comparison failed, expecting [" + expected + "], actual was [" + actual + "]";
272 }
273 else
274 {
275 msg = "XPathContains assertion failed for path [" + path + "] : " + e.getClass().getSimpleName() + ":"
276 + e.getMessage();
277 }
278
279 throw new AssertionException( new AssertionError( msg ) );
280 }
281
282 return type + " matches content for [" + path + "]";
283 }
284
285 private void compareValues( String expandedContent, String expandedValue, XmlObject object ) throws Exception
286 {
287 Diff diff = new Diff( expandedContent, expandedValue );
288 InternalDifferenceListener internalDifferenceListener = new InternalDifferenceListener();
289 diff.overrideDifferenceListener( internalDifferenceListener );
290
291 if( !diff.identical() )
292 throw new Exception( diff.toString() );
293
294 StringList nodesToRemove = internalDifferenceListener.getNodesToRemove();
295
296 if( !nodesToRemove.isEmpty() )
297 {
298 for( String node : nodesToRemove )
299 {
300 if( node == null )
301 continue;
302
303 int ix = node.indexOf( "\n/" );
304 if( ix != -1 )
305 node = node.substring( 0, ix + 1 ) + "/" + node.substring( ix + 1 );
306 else if( node.startsWith( "/" ) )
307 node = "/" + node;
308
309 XmlObject[] paths = object.selectPath( node );
310 if( paths.length > 0 )
311 {
312 Node domNode = paths[0].getDomNode();
313 if( domNode.getNodeType() == Node.ATTRIBUTE_NODE )
314 ( (Attr) domNode ).getOwnerElement().removeAttributeNode( (Attr) domNode );
315 else
316 domNode.getParentNode().removeChild( domNode );
317
318 object.set( object.copy() );
319 }
320 }
321 }
322 }
323
324 @Override
325 public boolean configure()
326 {
327 if( configurationDialog == null )
328 buildConfigurationDialog();
329
330 pathArea.setText( path );
331 contentArea.setText( expectedContent );
332 allowWildcardsCheckBox.setSelected( allowWildcards );
333
334 UISupport.showDialog( configurationDialog );
335 return configureResult;
336 }
337
338 protected void buildConfigurationDialog()
339 {
340 configurationDialog = new JDialog( UISupport.getMainFrame() );
341 configurationDialog.setTitle( "XPath Match configuration" );
342 configurationDialog.addWindowListener( new WindowAdapter()
343 {
344 @Override
345 public void windowOpened( WindowEvent event )
346 {
347 SwingUtilities.invokeLater( new Runnable()
348 {
349 public void run()
350 {
351 pathArea.requestFocusInWindow();
352 }
353 } );
354 }
355 } );
356
357 JPanel contentPanel = new JPanel( new BorderLayout() );
358 contentPanel.add( UISupport.buildDescription( "Specify xpath expression and expected result",
359 "declare namespaces with <code>declare namespace <prefix>='<namespace>';</code>", null ),
360 BorderLayout.NORTH );
361
362 JSplitPane splitPane = UISupport.createVerticalSplit();
363
364 pathArea = new JUndoableTextArea();
365 pathArea.setToolTipText( "Specifies the XPath expression to select from the message for validation" );
366
367 JPanel pathPanel = new JPanel( new BorderLayout() );
368 JXToolBar pathToolbar = UISupport.createToolbar();
369 addPathEditorActions( pathToolbar );
370
371 pathPanel.add( pathToolbar, BorderLayout.NORTH );
372 pathPanel.add( new JScrollPane( pathArea ), BorderLayout.CENTER );
373
374 splitPane.setTopComponent( UISupport.addTitledBorder( pathPanel, "XPath Expression" ) );
375
376 contentArea = new JUndoableTextArea();
377 contentArea.setToolTipText( "Specifies the expected result of the XPath expression" );
378
379 JPanel matchPanel = new JPanel( new BorderLayout() );
380 JXToolBar contentToolbar = UISupport.createToolbar();
381 addMatchEditorActions( contentToolbar );
382
383 matchPanel.add( contentToolbar, BorderLayout.NORTH );
384 matchPanel.add( new JScrollPane( contentArea ), BorderLayout.CENTER );
385
386 splitPane.setBottomComponent( UISupport.addTitledBorder( matchPanel, "Expected Result" ) );
387 splitPane.setDividerLocation( 150 );
388 splitPane.setBorder( BorderFactory.createEmptyBorder( 0, 1, 0, 1 ) );
389
390 contentPanel.add( splitPane, BorderLayout.CENTER );
391
392 ButtonBarBuilder builder = new ButtonBarBuilder();
393
394 ShowOnlineHelpAction showOnlineHelpAction = new ShowOnlineHelpAction( HelpUrls.XPATHASSERTIONEDITOR_HELP_URL );
395 builder.addFixed( UISupport.createToolbarButton( showOnlineHelpAction ) );
396 builder.addGlue();
397
398 JButton okButton = new JButton( new OkAction() );
399 builder.addFixed( okButton );
400 builder.addRelatedGap();
401 builder.addFixed( new JButton( new CancelAction() ) );
402
403 builder.setBorder( BorderFactory.createEmptyBorder( 1, 5, 5, 5 ) );
404
405 contentPanel.add( builder.getPanel(), BorderLayout.SOUTH );
406
407 configurationDialog.setContentPane( contentPanel );
408 configurationDialog.setSize( 600, 500 );
409 configurationDialog.setModal( true );
410 UISupport.initDialogActions( configurationDialog, showOnlineHelpAction, okButton );
411 }
412
413 protected void addPathEditorActions( JXToolBar toolbar )
414 {
415 toolbar.addFixed( new JButton( new DeclareNamespacesFromCurrentAction() ) );
416 }
417
418 protected void addMatchEditorActions( JXToolBar toolbar )
419 {
420 toolbar.addFixed( new JButton( new SelectFromCurrentAction() ) );
421 toolbar.addRelatedGap();
422 toolbar.addFixed( new JButton( new TestPathAction() ) );
423 allowWildcardsCheckBox = new JCheckBox( "Allow Wildcards" );
424
425 Dimension dim = new Dimension( 100, 20 );
426
427 allowWildcardsCheckBox.setSize( dim );
428 allowWildcardsCheckBox.setPreferredSize( dim );
429
430 allowWildcardsCheckBox.setOpaque( false );
431 toolbar.addRelatedGap();
432 toolbar.addFixed( allowWildcardsCheckBox );
433
434 Dimension largerDim = new Dimension( 200, 20 );
435 ignoreNamspaceDifferencesCheckBox = new JCheckBox( "Ignore namespace prefixes" );
436 ignoreNamspaceDifferencesCheckBox.setSize( largerDim );
437 ignoreNamspaceDifferencesCheckBox.setPreferredSize( largerDim );
438 ignoreNamspaceDifferencesCheckBox.setOpaque( false );
439 toolbar.addRelatedGap();
440 toolbar.addFixed( ignoreNamspaceDifferencesCheckBox );
441 }
442
443 public XmlObject createConfiguration()
444 {
445 XmlObjectConfigurationBuilder builder = new XmlObjectConfigurationBuilder();
446 builder.add( "path", path );
447 builder.add( "content", expectedContent );
448 builder.add( "allowWildcards", allowWildcards );
449 builder.add( "ignoreNamspaceDifferences", ignoreNamspaceDifferences );
450 return builder.finish();
451 }
452
453 public void selectFromCurrent()
454 {
455 XmlCursor cursor = null;
456
457 try
458 {
459 String assertableContent = getAssertable().getAssertableContent();
460 if( assertableContent == null || assertableContent.trim().length() == 0 )
461 {
462 UISupport.showErrorMessage( "Missing content to select from" );
463 return;
464 }
465
466 XmlObject xml = XmlObject.Factory.parse( assertableContent );
467
468 String txt = pathArea == null || !pathArea.isVisible() ? getPath() : pathArea.getSelectedText();
469 if( txt == null )
470 txt = pathArea == null ? "" : pathArea.getText();
471
472 WsdlTestRunContext context = new WsdlTestRunContext( (TestStep) getAssertable().getModelItem() );
473
474 String expandedPath = PropertyExpansionUtils.expandProperties( context, txt.trim() );
475
476 if( contentArea != null && contentArea.isVisible() )
477 contentArea.setText( "" );
478
479 cursor = xml.newCursor();
480 cursor.selectPath( expandedPath );
481 if( !cursor.toNextSelection() )
482 {
483 UISupport.showErrorMessage( "No match in current response" );
484 }
485 else if( cursor.hasNextSelection() )
486 {
487 UISupport.showErrorMessage( "More than one match in current response" );
488 }
489 else
490 {
491 String stringValue = XmlUtils.getValueForMatch( cursor );
492
493 if( contentArea != null && contentArea.isVisible() )
494 contentArea.setText( stringValue );
495 else
496 setExpectedContent( stringValue );
497 }
498 }
499 catch( Throwable e )
500 {
501 UISupport.showErrorMessage( e.toString() );
502 SoapUI.logError( e );
503 }
504 finally
505 {
506 if( cursor != null )
507 cursor.dispose();
508 }
509 }
510
511 private final class InternalDifferenceListener implements DifferenceListener
512 {
513 private StringList nodesToRemove = new StringList();
514
515 public int differenceFound( Difference diff )
516 {
517 if( allowWildcards
518 && ( diff.getId() == DifferenceEngine.TEXT_VALUE.getId() || diff.getId() == DifferenceEngine.ATTR_VALUE.getId() ) )
519 {
520 if( diff.getControlNodeDetail().getValue().equals( "*" ) )
521 {
522 Node node = diff.getTestNodeDetail().getNode();
523 String xp = XmlUtils.createAbsoluteXPath( node.getNodeType() == Node.ATTRIBUTE_NODE ? node : node.getParentNode() );
524 nodesToRemove.add( xp );
525 return Diff.RETURN_IGNORE_DIFFERENCE_NODES_IDENTICAL;
526 }
527 }
528 else if( ignoreNamspaceDifferences && diff.getId() == DifferenceEngine.NAMESPACE_PREFIX_ID )
529 {
530 return Diff.RETURN_IGNORE_DIFFERENCE_NODES_IDENTICAL;
531 }
532
533 return Diff.RETURN_ACCEPT_DIFFERENCE;
534 }
535
536 public void skippedComparison( Node arg0, Node arg1 )
537 {
538
539 }
540
541 public StringList getNodesToRemove()
542 {
543 return nodesToRemove;
544 }
545 }
546
547 public class OkAction extends AbstractAction
548 {
549 public OkAction()
550 {
551 super( "Save" );
552 }
553
554 public void actionPerformed( ActionEvent arg0 )
555 {
556 setPath( pathArea.getText().trim() );
557 setExpectedContent( contentArea.getText() );
558 setAllowWildcards( allowWildcardsCheckBox.isSelected() );
559 setIgnoreNamspaceDifferences( ignoreNamspaceDifferencesCheckBox.isSelected() );
560 setConfiguration( createConfiguration() );
561 configureResult = true;
562 configurationDialog.setVisible( false );
563 }
564 }
565
566 public class CancelAction extends AbstractAction
567 {
568 public CancelAction()
569 {
570 super( "Cancel" );
571 }
572
573 public void actionPerformed( ActionEvent arg0 )
574 {
575 configureResult = false;
576 configurationDialog.setVisible( false );
577 }
578 }
579
580 public class DeclareNamespacesFromCurrentAction extends AbstractAction
581 {
582 public DeclareNamespacesFromCurrentAction()
583 {
584 super( "Declare" );
585 putValue( Action.SHORT_DESCRIPTION, "Add namespace declaration from current message to XPath expression" );
586 }
587
588 public void actionPerformed( ActionEvent arg0 )
589 {
590 try
591 {
592 String content = getAssertable().getAssertableContent();
593 if( content != null && content.trim().length() > 0 )
594 {
595 pathArea.setText( XmlUtils.declareXPathNamespaces( content ) + pathArea.getText() );
596 }
597 else if( UISupport.confirm( "Declare namespaces from schema instead?", "Missing Response" ) )
598 {
599 pathArea.setText( XmlUtils.declareXPathNamespaces( (WsdlInterface) getAssertable().getInterface() )
600 + pathArea.getText() );
601 }
602 }
603 catch( Exception e )
604 {
605 log.error( e.getMessage() );
606 }
607 }
608 }
609
610 public class TestPathAction extends AbstractAction
611 {
612 public TestPathAction()
613 {
614 super( "Test" );
615 putValue( Action.SHORT_DESCRIPTION,
616 "Tests the XPath expression for the current message against the Expected Content field" );
617 }
618
619 public void actionPerformed( ActionEvent arg0 )
620 {
621 String oldPath = getPath();
622 String oldContent = getExpectedContent();
623 boolean oldAllowWildcards = isAllowWildcards();
624
625 setPath( pathArea.getText().trim() );
626 setExpectedContent( contentArea.getText() );
627 setAllowWildcards( allowWildcardsCheckBox.isSelected() );
628 setIgnoreNamspaceDifferences( ignoreNamspaceDifferencesCheckBox.isSelected() );
629
630 try
631 {
632 String msg = assertContent( getAssertable().getAssertableContent(), new WsdlTestRunContext( (TestStep) getAssertable()
633 .getModelItem() ), "Response" );
634 UISupport.showInfoMessage( msg, "Success" );
635 }
636 catch( AssertionException e )
637 {
638 UISupport.showErrorMessage( e.getMessage() );
639 }
640
641 setPath( oldPath );
642 setExpectedContent( oldContent );
643 setAllowWildcards( oldAllowWildcards );
644 }
645 }
646
647 public class SelectFromCurrentAction extends AbstractAction
648 {
649 public SelectFromCurrentAction()
650 {
651 super( "Select from current" );
652 putValue( Action.SHORT_DESCRIPTION,
653 "Selects the XPath expression from the current message into the Expected Content field" );
654 }
655
656 public void actionPerformed( ActionEvent arg0 )
657 {
658 selectFromCurrent();
659 }
660 }
661
662 @Override
663 protected String internalAssertRequest( MessageExchange messageExchange, SubmitContext context )
664 throws AssertionException
665 {
666 if( !messageExchange.hasRequest( true ) )
667 return "Missing Request";
668 else
669 return assertContent( messageExchange.getRequestContent(), context, "Request" );
670 }
671
672 public JTextArea getContentArea()
673 {
674 return contentArea;
675 }
676
677 public JTextArea getPathArea()
678 {
679 return pathArea;
680 }
681
682 @Override
683 public PropertyExpansion[] getPropertyExpansions()
684 {
685 List<PropertyExpansion> result = new ArrayList<PropertyExpansion>();
686
687 result.addAll( PropertyExpansionUtils.extractPropertyExpansions( getAssertable().getModelItem(), this, "expectedContent" ) );
688 result.addAll( PropertyExpansionUtils.extractPropertyExpansions( getAssertable().getModelItem(), this, "path" ) );
689
690 return result.toArray( new PropertyExpansion[result.size()] );
691 }
692
693 public XPathReference[] getXPathReferences()
694 {
695 List<XPathReference> result = new ArrayList<XPathReference>();
696
697 if( StringUtils.hasContent( getPath() ) )
698 {
699 TestModelItem testStep = (TestModelItem) getAssertable().getModelItem();
700 TestProperty property = testStep instanceof WsdlTestRequestStep ? testStep.getProperty( "Response" ) : testStep.getProperty( "Request" );
701 result.add( new XPathReferenceImpl( "XPath for " + getName() + " XPathContainsAssertion in " + testStep.getName(), property, this, "path" ) );
702 }
703
704 return result.toArray( new XPathReference[result.size()] );
705 }
706
707 public static class Factory extends AbstractTestAssertionFactory
708 {
709 public Factory()
710 {
711 super( XPathContainsAssertion.ID, XPathContainsAssertion.LABEL, XPathContainsAssertion.class );
712 }
713 }
714 }