KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > eclipse > compare > structuremergeviewer > StructureDiffViewer


1 /*******************************************************************************
2  * Copyright (c) 2000, 2007 IBM Corporation and others.
3  * All rights reserved. This program and the accompanying materials
4  * are made available under the terms of the Eclipse Public License v1.0
5  * which accompanies this distribution, and is available at
6  * http://www.eclipse.org/legal/epl-v10.html
7  *
8  * Contributors:
9  * IBM Corporation - initial API and implementation
10  *******************************************************************************/

11 package org.eclipse.compare.structuremergeviewer;
12
13 import java.lang.reflect.InvocationTargetException JavaDoc;
14
15 import org.eclipse.compare.*;
16 import org.eclipse.compare.contentmergeviewer.IDocumentRange;
17 import org.eclipse.compare.internal.*;
18 import org.eclipse.core.runtime.*;
19 import org.eclipse.jface.operation.IRunnableWithProgress;
20 import org.eclipse.jface.text.BadPositionCategoryException;
21 import org.eclipse.jface.text.IDocument;
22 import org.eclipse.jface.util.PropertyChangeEvent;
23 import org.eclipse.swt.custom.BusyIndicator;
24 import org.eclipse.swt.events.DisposeEvent;
25 import org.eclipse.swt.widgets.*;
26 import org.eclipse.ui.services.IDisposable;
27
28
29 /**
30  * A diff tree viewer that can be configured with a <code>IStructureCreator</code>
31  * to retrieve a hierarchical structure from the input object (an <code>ICompareInput</code>)
32  * and perform a two-way or three-way compare on it.
33  * <p>
34  * This class may be instantiated; it is not intended to be subclassed outside
35  * this package.
36  * </p>
37  *
38  * @see IStructureCreator
39  * @see ICompareInput
40  */

41         
42 public class StructureDiffViewer extends DiffTreeViewer {
43     private Differencer fDifferencer;
44     private boolean fThreeWay= false;
45     
46     private StructureInfo fAncestorStructure = new StructureInfo();
47     private StructureInfo fLeftStructure = new StructureInfo();
48     private StructureInfo fRightStructure = new StructureInfo();
49     
50     private IStructureCreator fStructureCreator;
51     private IDiffContainer fRoot;
52     private IContentChangeListener fContentChangedListener;
53     private CompareViewerSwitchingPane fParent;
54     private ICompareInputChangeListener fCompareInputChangeListener;
55     
56     /*
57      * A set of background tasks for updating the structure
58      */

59     private IRunnableWithProgress diffTask = new IRunnableWithProgress() {
60         public void run(IProgressMonitor monitor) throws InvocationTargetException JavaDoc,
61                 InterruptedException JavaDoc {
62             monitor.beginTask(CompareMessages.StructureDiffViewer_0, 100);
63             diff(new SubProgressMonitor(monitor, 100));
64             monitor.done();
65         }
66     };
67     
68     private IRunnableWithProgress inputChangedTask = new IRunnableWithProgress() {
69         public void run(IProgressMonitor monitor) throws InvocationTargetException JavaDoc,
70                 InterruptedException JavaDoc {
71             monitor.beginTask(CompareMessages.StructureDiffViewer_1, 100);
72             // TODO: Should we always force
73
compareInputChanged((ICompareInput)getInput(), true, new SubProgressMonitor(monitor, 100));
74             monitor.done();
75         }
76     };
77     
78     /*
79      * A helper class for holding the input and generated structure
80      * for the ancestor, left and right inputs.
81      */

82     private class StructureInfo {
83         private ITypedElement fInput;
84         private IStructureComparator fStructureComparator;
85         private IRunnableWithProgress refreshTask = new IRunnableWithProgress() {
86             public void run(IProgressMonitor monitor) throws InvocationTargetException JavaDoc,
87                     InterruptedException JavaDoc {
88                 refresh(monitor);
89             }
90         };
91         
92         public boolean setInput(ITypedElement newInput, boolean force, IProgressMonitor monitor) {
93             boolean changed = false;
94             if (force || newInput != fInput) {
95                 removeDocumentRangeUpdaters();
96                 if (fInput instanceof IContentChangeNotifier && fContentChangedListener != null)
97                     ((IContentChangeNotifier)fInput).removeContentChangeListener(fContentChangedListener);
98                 fInput= newInput;
99                 if (fInput == null) {
100                     if (fStructureComparator instanceof IDisposable) {
101                         IDisposable disposable = (IDisposable) fStructureComparator;
102                         disposable.dispose();
103                     }
104                     fStructureComparator= null;
105                 } else {
106                     refresh(monitor);
107                     changed= true;
108                 }
109                 if (fInput instanceof IContentChangeNotifier && fContentChangedListener != null)
110                     ((IContentChangeNotifier)fInput).addContentChangeListener(fContentChangedListener);
111             }
112             return changed;
113         }
114
115         /**
116          * Remove any document range updaters that were registered against the document.
117          */

118         private void removeDocumentRangeUpdaters() {
119             if (fStructureComparator instanceof IDocumentRange) {
120                 IDocument doc = ((IDocumentRange) fStructureComparator).getDocument();
121                 try {
122                     doc.removePositionCategory(IDocumentRange.RANGE_CATEGORY);
123                 } catch (BadPositionCategoryException ex) {
124                     // Ignore
125
}
126             }
127         }
128         
129         public IStructureComparator getStructureComparator() {
130             return fStructureComparator;
131         }
132
133         public void refresh(IProgressMonitor monitor) {
134             IStructureComparator oldComparator = fStructureComparator;
135             fStructureComparator= createStructure(monitor);
136             // Dispose of the old one after in case they are using a shared document
137
// (i.e. disposing it after will hold on to a reference to the document
138
// so it doesn't get freed and reloaded)
139
if (oldComparator instanceof IDisposable) {
140                 IDisposable disposable = (IDisposable) oldComparator;
141                 disposable.dispose();
142             }
143         }
144
145         public Object JavaDoc getInput() {
146             return fInput;
147         }
148         
149         private IStructureComparator createStructure(IProgressMonitor monitor) {
150             // Defend against concurrent disposal
151
Object JavaDoc input = fInput;
152             if (input == null)
153                 return null;
154             if (fStructureCreator instanceof IStructureCreator2) {
155                 IStructureCreator2 sc2 = (IStructureCreator2) fStructureCreator;
156                 try {
157                     return sc2.createStructure(input, monitor);
158                 } catch (CoreException e) {
159                     CompareUIPlugin.log(e);
160                 }
161             }
162             return fStructureCreator.getStructure(input);
163         }
164
165         public void dispose() {
166             if (fStructureComparator != null && fStructureCreator instanceof IStructureCreator2) {
167                 IStructureCreator2 sc2 = (IStructureCreator2) fStructureCreator;
168                 sc2.destroy(fStructureComparator);
169             }
170         }
171
172         public IRunnableWithProgress getRefreshTask() {
173             return refreshTask;
174         }
175     }
176     
177     /**
178      * Creates a new viewer for the given SWT tree control with the specified configuration.
179      *
180      * @param tree the tree control
181      * @param configuration the configuration for this viewer
182      */

183     public StructureDiffViewer(Tree tree, CompareConfiguration configuration) {
184         super(tree, configuration);
185         Composite c= tree.getParent();
186         if (c instanceof CompareViewerSwitchingPane)
187             fParent= (CompareViewerSwitchingPane) c;
188         initialize();
189     }
190     
191     /**
192      * Creates a new viewer under the given SWT parent with the specified configuration.
193      *
194      * @param parent the SWT control under which to create the viewer
195      * @param configuration the configuration for this viewer
196      */

197     public StructureDiffViewer(Composite parent, CompareConfiguration configuration) {
198         super(parent, configuration);
199         if (parent instanceof CompareViewerSwitchingPane)
200             fParent= (CompareViewerSwitchingPane) parent;
201         initialize();
202     }
203     
204     private void initialize() {
205         
206         setAutoExpandLevel(3);
207         
208         fContentChangedListener= new IContentChangeListener() {
209             public void contentChanged(IContentChangeNotifier changed) {
210                 StructureDiffViewer.this.contentChanged(changed);
211             }
212         };
213         fCompareInputChangeListener = new ICompareInputChangeListener() {
214             public void compareInputChanged(ICompareInput input) {
215                 StructureDiffViewer.this.compareInputChanged(input, true);
216             }
217         };
218     }
219     
220     /**
221      * Configures the <code>StructureDiffViewer</code> with a structure creator.
222      * The structure creator is used to create a hierarchical structure
223      * for each side of the viewer's input element of type <code>ICompareInput</code>.
224      *
225      * @param structureCreator the new structure creator
226      */

227     public void setStructureCreator(IStructureCreator structureCreator) {
228         if (fStructureCreator != structureCreator) {
229             fStructureCreator= structureCreator;
230             Control tree= getControl();
231             if (tree != null && !tree.isDisposed())
232                 tree.setData(CompareUI.COMPARE_VIEWER_TITLE, getTitle());
233         }
234     }
235     
236     /**
237      * Returns the structure creator or <code>null</code> if no
238      * structure creator has been set with <code>setStructureCreator</code>.
239      *
240      * @return the structure creator or <code>null</code>
241      */

242     public IStructureCreator getStructureCreator() {
243         return fStructureCreator;
244     }
245     
246     /**
247      * Reimplemented to get the descriptive title for this viewer from the <code>IStructureCreator</code>.
248      * @return the viewer's name
249      */

250     public String JavaDoc getTitle() {
251         if (fStructureCreator != null)
252             return fStructureCreator.getName();
253         return super.getTitle();
254     }
255     
256     /**
257      * Overridden because the input of this viewer is not identical to the root of the tree.
258      * The tree's root is a IDiffContainer that was returned from the method <code>diff</code>.
259      *
260      * @return the root of the diff tree produced by method <code>diff</code>
261      */

262     protected Object JavaDoc getRoot() {
263         return fRoot;
264     }
265     
266     /*
267      * (non-Javadoc) Method declared on StructuredViewer.
268      * Overridden to create the comparable structures from the input object
269      * and to feed them through the differencing engine. Note: for this viewer
270      * the value from <code>getInput</code> is not identical to <code>getRoot</code>.
271      */

272     protected void inputChanged(Object JavaDoc input, Object JavaDoc oldInput) {
273         if (oldInput instanceof ICompareInput) {
274             ICompareInput old = (ICompareInput) oldInput;
275             old.removeCompareInputChangeListener(fCompareInputChangeListener);
276         }
277         if (input instanceof ICompareInput) {
278             ICompareInput ci = (ICompareInput) input;
279             ci.addCompareInputChangeListener(fCompareInputChangeListener);
280             compareInputChanged(ci);
281             if (input != oldInput)
282                 initialSelection();
283         }
284     }
285     
286     protected void initialSelection() {
287         expandToLevel(2);
288     }
289
290     /* (non Javadoc)
291      * Overridden to unregister all listeners.
292      */

293     protected void handleDispose(DisposeEvent event) {
294         Object JavaDoc input = getInput();
295         if (input instanceof ICompareInput) {
296             ICompareInput ci = (ICompareInput) input;
297             ci.removeCompareInputChangeListener(fCompareInputChangeListener);
298         }
299         compareInputChanged(null);
300         fContentChangedListener= null;
301         super.handleDispose(event);
302     }
303     
304     /**
305      * Recreates the comparable structures for the input sides.
306      * @param input this viewer's new input
307      */

308     protected void compareInputChanged(ICompareInput input) {
309         compareInputChanged(input, false);
310     }
311         
312     /* package */ void compareInputChanged(final ICompareInput input, final boolean force) {
313         if (input == null) {
314             // When closing, we don't need a progress monitor to handle the input change
315
compareInputChanged(input, force, null);
316             return;
317         }
318         CompareConfiguration cc = getCompareConfiguration();
319         // The compare configuration is nulled when the viewer is disposed
320
if (cc != null) {
321             BusyIndicator.showWhile(Display.getDefault(), new Runnable JavaDoc() {
322                 public void run() {
323                     try {
324                         inputChangedTask.run(new NullProgressMonitor());
325                     } catch (InvocationTargetException JavaDoc e) {
326                         CompareUIPlugin.log(e.getTargetException());
327                     } catch (InterruptedException JavaDoc e) {
328                         // Ignore
329
}
330                 }
331             });
332         }
333     }
334
335     /* package */ void compareInputChanged(ICompareInput input, boolean force, IProgressMonitor monitor) {
336         ITypedElement t= null;
337         boolean changed= false;
338         
339         if (input != null)
340             t= input.getAncestor();
341         fThreeWay= (t != null);
342         beginWork(monitor, 400);
343         try {
344             if (fAncestorStructure.setInput(t, force, subMonitor(monitor, 100)))
345                 changed = true;
346             
347             if (input != null)
348                 t= input.getLeft();
349             if (fLeftStructure.setInput(t, force, subMonitor(monitor, 100)))
350                 changed = true;
351             
352             if (input != null)
353                 t= input.getRight();
354             if (fRightStructure.setInput(t, force, subMonitor(monitor, 100)))
355                 changed = true;
356             
357             // The compare configuration is nulled when the viewer is disposed
358
CompareConfiguration cc = getCompareConfiguration();
359             if (changed && cc != null)
360                 cc.getContainer().runAsynchronously(diffTask);
361         } finally {
362             endWork(monitor);
363         }
364     }
365     
366     private void endWork(IProgressMonitor monitor) {
367         if (monitor != null)
368             monitor.done();
369     }
370
371     private IProgressMonitor subMonitor(IProgressMonitor monitor, int work) {
372         if (monitor != null) {
373             if (monitor.isCanceled() || getControl().isDisposed())
374                 throw new OperationCanceledException();
375             return new SubProgressMonitor(monitor, work);
376         }
377         return null;
378     }
379
380     private void beginWork(IProgressMonitor monitor, int totalWork) {
381         if (monitor != null)
382             monitor.beginTask(null, totalWork);
383     }
384
385     /**
386      * Calls <code>diff</code> whenever the byte contents changes.
387      * @param changed the object that sent out the notification
388      */

389     protected void contentChanged(final IContentChangeNotifier changed) {
390         
391         if (fStructureCreator == null)
392             return;
393         
394         if (changed == null) {
395             getCompareConfiguration().getContainer().runAsynchronously(fAncestorStructure.getRefreshTask());
396             getCompareConfiguration().getContainer().runAsynchronously(fLeftStructure.getRefreshTask());
397             getCompareConfiguration().getContainer().runAsynchronously(fRightStructure.getRefreshTask());
398         } else if (changed == fAncestorStructure.getInput()) {
399             getCompareConfiguration().getContainer().runAsynchronously(fAncestorStructure.getRefreshTask());
400         } else if (changed == fLeftStructure.getInput()) {
401             getCompareConfiguration().getContainer().runAsynchronously(fLeftStructure.getRefreshTask());
402         } else if (changed == fRightStructure.getInput()) {
403             getCompareConfiguration().getContainer().runAsynchronously(fRightStructure.getRefreshTask());
404         } else {
405             return;
406         }
407         getCompareConfiguration().getContainer().runAsynchronously(diffTask);
408     }
409
410     /**
411      * This method is called from within <code>diff()</code> before the
412      * difference tree is being built. Clients may override this method to
413      * perform their own pre-processing. This default implementation does
414      * nothing.
415      *
416      * @param ancestor the ancestor input to the differencing operation
417      * @param left the left input to the differencing operation
418      * @param right the right input to the differencing operation
419      * @since 2.0
420      * @deprecated Clients should override
421      * {@link #preDiffHook(IStructureComparator, IStructureComparator, IStructureComparator, IProgressMonitor)}
422      */

423     protected void preDiffHook(IStructureComparator ancestor, IStructureComparator left, IStructureComparator right) {
424         // we do nothing here
425
}
426     
427     /**
428      * This method is called from within {@link #diff(IProgressMonitor)} before
429      * the difference tree is being built. This method may be called from a
430      * background (non-UI) thread).
431      * <p>
432      * For backwards compatibility, this default implementation calls
433      * {@link #preDiffHook(IStructureComparator, IStructureComparator, IStructureComparator)}
434      * from the UI thread. Clients should override this method even if they
435      * don't perform pre-processing to avoid the call to the UI thread.
436      *
437      * @param ancestor the ancestor input to the differencing operation
438      * @param left the left input to the differencing operation
439      * @param right the right input to the differencing operation
440      * @param monitor a progress monitor or null if progress is not required
441      * @since 3.3
442      */

443     protected void preDiffHook(final IStructureComparator ancestor, final IStructureComparator left, final IStructureComparator right, IProgressMonitor monitor) {
444         syncExec(new Runnable JavaDoc() {
445             public void run() {
446                 preDiffHook(ancestor, left, right);
447             }
448         });
449     }
450
451     /**
452      * Runs the difference engine and refreshes the tree. This method may be called
453      * from a background (non-UI) thread).
454      * @param monitor a progress monitor or <code>null</code> if progress in not required
455      */

456     protected void diff(IProgressMonitor monitor) {
457         try {
458             beginWork(monitor, 150);
459             
460             IStructureComparator ancestorComparator = fAncestorStructure.getStructureComparator();
461             IStructureComparator leftComparator = fLeftStructure.getStructureComparator();
462             IStructureComparator rightComparator = fRightStructure.getStructureComparator();
463             
464             preDiffHook(ancestorComparator,
465                     leftComparator,
466                     rightComparator,
467                     subMonitor(monitor, 25));
468                                 
469             String JavaDoc message= null;
470             
471             if ((fThreeWay && ancestorComparator == null) || leftComparator == null || rightComparator == null) {
472                 // could not get structure of one (or more) of the legs
473
fRoot= null;
474                 message= CompareMessages.StructureDiffViewer_StructureError;
475                 
476             } else { // calculate difference of the two (or three) structures
477

478                 if (fDifferencer == null)
479                     fDifferencer= new Differencer() {
480                         protected boolean contentsEqual(Object JavaDoc o1, Object JavaDoc o2) {
481                             return StructureDiffViewer.this.contentsEqual(o1, o2);
482                         }
483                         protected Object JavaDoc visit(Object JavaDoc data, int result, Object JavaDoc ancestor, Object JavaDoc left, Object JavaDoc right) {
484                             Object JavaDoc o= super.visit(data, result, ancestor, left, right);
485                             if (fLeftIsLocal && o instanceof DiffNode)
486                                 ((DiffNode)o).swapSides(fLeftIsLocal);
487                             return o;
488                         }
489                     };
490                 
491                 fRoot= (IDiffContainer) fDifferencer.findDifferences(fThreeWay, subMonitor(monitor, 100), null,
492                         ancestorComparator, leftComparator, rightComparator);
493                         
494                 if (fRoot == null || fRoot.getChildren().length == 0) {
495                     message= CompareMessages.StructureDiffViewer_NoStructuralDifferences;
496                 } else {
497                     postDiffHook(fDifferencer, fRoot, subMonitor(monitor, 25));
498                 }
499             }
500             
501             if (Display.getCurrent() != null)
502                 refreshAfterDiff(message);
503             else {
504                 final String JavaDoc theMessage = message;
505                 Display.getDefault().asyncExec(new Runnable JavaDoc() {
506                     public void run() {
507                         refreshAfterDiff(theMessage);
508                     }
509                 });
510             }
511         } finally {
512             endWork(monitor);
513         }
514     }
515
516     private void refreshAfterDiff(String JavaDoc message) {
517         if (getControl().isDisposed())
518             return;
519         if (fParent != null)
520             fParent.setTitleArgument(message);
521         
522         refresh(getRoot());
523         // Setting the auto-expand level doesn't do anything for refreshes
524
expandToLevel(3);
525     }
526     
527     /**
528      * Runs the difference engine and refreshes the tree.
529      */

530     protected void diff() {
531         try {
532             CompareConfiguration compareConfiguration = getCompareConfiguration();
533             // A null compare configuration indicates that the viewer was disposed
534
if (compareConfiguration != null) {
535                 compareConfiguration.getContainer().run(true, true, new IRunnableWithProgress() {
536                     public void run(IProgressMonitor monitor) throws InvocationTargetException JavaDoc, InterruptedException JavaDoc {
537                         monitor.beginTask(CompareMessages.StructureDiffViewer_2, 100);
538                         diffTask.run(new SubProgressMonitor(monitor, 100));
539                         monitor.done();
540                     }
541                 });
542             }
543         } catch (InvocationTargetException JavaDoc e) {
544             // Shouldn't happen since the run doesn't throw
545
CompareUIPlugin.log(e.getTargetException());
546             handleFailedRefresh(e.getTargetException().getMessage());
547         } catch (InterruptedException JavaDoc e) {
548             // Canceled by user
549
handleFailedRefresh(CompareMessages.StructureDiffViewer_3);
550         }
551     }
552     
553     private void handleFailedRefresh(final String JavaDoc message) {
554         Runnable JavaDoc runnable = new Runnable JavaDoc() {
555             public void run() {
556                 if (getControl().isDisposed())
557                     return;
558                 refreshAfterDiff(message);
559             }
560         };
561         if (Display.getCurrent() != null)
562             runnable.run();
563         else
564             Display.getDefault().asyncExec(runnable);
565     }
566
567     /**
568      * This method is called from within <code>diff()</code> after the
569      * difference tree has been built. Clients may override this method to
570      * perform their own post-processing. This default implementation does
571      * nothing.
572      *
573      * @param differencer the differencer used to perform the differencing
574      * @param root the non-<code>null</code> root node of the difference tree
575      * @since 2.0
576      * @deprecated Subclasses should override
577      * {@link #postDiffHook(Differencer, IDiffContainer, IProgressMonitor)}
578      * instead
579      */

580     protected void postDiffHook(Differencer differencer, IDiffContainer root) {
581         // we do nothing here
582
}
583     
584     /**
585      * This method is called from within {@link #diff(IProgressMonitor)} after
586      * the difference tree has been built. This method may be called from a
587      * background (non-UI) thread).
588      * <p>
589      * For backwards compatibility, this default implementation calls
590      * {@link #postDiffHook(Differencer, IDiffContainer)} from the UI thread.
591      * Clients should override this method even if they don't perform post
592      * processing to avoid the call to the UI thread.
593      *
594      * @param differencer the differencer used to perform the differencing
595      * @param root the non-<code>null</code> root node of the difference tree
596      * @param monitor a progress monitor or <code>null</code> if progress is
597      * not required
598      * @since 3.3
599      */

600     protected void postDiffHook(final Differencer differencer, final IDiffContainer root, IProgressMonitor monitor) {
601         syncExec(new Runnable JavaDoc() {
602             public void run() {
603                 postDiffHook(differencer, root);
604             }
605         });
606     }
607     
608     /*
609      * Performs a byte compare on the given objects.
610      * Called from the difference engine.
611      * Returns <code>null</code> if no structure creator has been set.
612      */

613     private boolean contentsEqual(Object JavaDoc o1, Object JavaDoc o2) {
614         if (fStructureCreator != null) {
615             boolean ignoreWhiteSpace= Utilities.getBoolean(getCompareConfiguration(), CompareConfiguration.IGNORE_WHITESPACE, false);
616             String JavaDoc s1= fStructureCreator.getContents(o1, ignoreWhiteSpace);
617             String JavaDoc s2= fStructureCreator.getContents(o2, ignoreWhiteSpace);
618             if (s1 == null || s2 == null)
619                 return false;
620             return s1.equals(s2);
621         }
622         return false;
623     }
624     
625     /**
626      * Tracks property changes of the configuration object.
627      * Clients may override to track their own property changes.
628      * In this case they must call the inherited method.
629      * @param event the property changed event that triggered the call to this method
630      */

631     protected void propertyChange(PropertyChangeEvent event) {
632         String JavaDoc key= event.getProperty();
633         if (key.equals(CompareConfiguration.IGNORE_WHITESPACE))
634             diff();
635         else
636             super.propertyChange(event);
637     }
638         
639     /**
640      * Overridden to call the <code>save</code> method on the structure creator after
641      * nodes have been copied from one side to the other side of an input object.
642      *
643      * @param leftToRight if <code>true</code> the left side is copied to the right side.
644      * If <code>false</code> the right side is copied to the left side
645      */

646     protected void copySelected(boolean leftToRight) {
647         super.copySelected(leftToRight);
648         
649         if (fStructureCreator != null)
650             fStructureCreator.save(
651                             leftToRight ? fRightStructure.getStructureComparator() : fLeftStructure.getStructureComparator(),
652                             leftToRight ? fRightStructure.getInput() : fLeftStructure.getInput());
653     }
654     
655     private void syncExec(final Runnable JavaDoc runnable) {
656         if (getControl().isDisposed())
657             return;
658         if (Display.getCurrent() != null)
659             runnable.run();
660         else
661             getControl().getDisplay().syncExec(new Runnable JavaDoc() {
662                 public void run() {
663                     if (!getControl().isDisposed())
664                         runnable.run();
665                 }
666             });
667     }
668 }
669
670
Popular Tags