KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > openide > text > DataEditorSupport


1 /*
2  * The contents of this file are subject to the terms of the Common Development
3  * and Distribution License (the License). You may not use this file except in
4  * compliance with the License.
5  *
6  * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
7  * or http://www.netbeans.org/cddl.txt.
8  *
9  * When distributing Covered Code, include this CDDL Header Notice in each file
10  * and include the License file at http://www.netbeans.org/cddl.txt.
11  * If applicable, add the following below the CDDL Header, with the fields
12  * enclosed by brackets [] replaced by your own identifying information:
13  * "Portions Copyrighted [year] [name of copyright owner]"
14  *
15  * The Original Software is NetBeans. The Initial Developer of the Original
16  * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
17  * Microsystems, Inc. All Rights Reserved.
18  */

19
20 package org.openide.text;
21
22
23 import java.beans.*;
24 import java.io.*;
25 import java.lang.ref.Reference JavaDoc;
26 import java.util.*;
27 import java.util.logging.Level JavaDoc;
28 import java.util.logging.Logger JavaDoc;
29 import javax.swing.text.*;
30 import org.netbeans.modules.openide.loaders.UIException;
31 import org.openide.*;
32 import org.openide.filesystems.*;
33 import org.openide.loaders.*;
34 import org.openide.nodes.*;
35 import org.openide.util.*;
36 import org.openide.util.lookup.*;
37 import org.openide.windows.CloneableOpenSupport;
38
39 /**
40  * Support for associating an editor and a Swing {@link Document} to a data object.
41  * @author Jaroslav Tulach
42  */

43 public class DataEditorSupport extends CloneableEditorSupport {
44     /** error manager for CloneableEditorSupport logging and error reporting */
45     static final Logger JavaDoc ERR = Logger.getLogger("org.openide.text.DataEditorSupport"); // NOI18N
46

47     /** Which data object we are associated with */
48     private final DataObject obj;
49     /** listener to asociated node's events */
50     private NodeListener nodeL;
51     
52     /** Editor support for a given data object. The file is taken from the
53     * data object and is updated if the object moves or renames itself.
54     * @param obj object to work with
55     * @param env environment to pass to
56     */

57     public DataEditorSupport (DataObject obj, CloneableEditorSupport.Env env) {
58         super (env, new DOEnvLookup (obj));
59         this.obj = obj;
60     }
61     
62     /** Getter for the environment that was provided in the constructor.
63     * @return the environment
64     */

65     final CloneableEditorSupport.Env desEnv() {
66         return (CloneableEditorSupport.Env) env;
67     }
68     
69     /** Factory method to create simple CloneableEditorSupport for a given
70      * entry of a given DataObject. The common use inside DataObject looks like
71      * this:
72      * <pre>
73      * getCookieSet().add((Node.Cookie) DataEditorSupport.create(this, getPrimaryEntry(), getCookieSet()));
74      * </pre>
75      *
76      * @param obj the data object
77      * @param entry the entry to read and write from
78      * @param set cookie set to add remove additional cookies (currently only {@link org.openide.cookies.SaveCookie})
79      * @return a subclass of DataEditorSupport that implements at least
80      * {@link org.openide.cookies.OpenCookie},
81      * {@link org.openide.cookies.EditCookie},
82      * {@link org.openide.cookies.EditorCookie.Observable},
83      * {@link org.openide.cookies.PrintCookie},
84      * {@link org.openide.cookies.CloseCookie}
85      * @since 5.2
86      */

87     public static CloneableEditorSupport create (DataObject obj, MultiDataObject.Entry entry, org.openide.nodes.CookieSet set) {
88         return new SimpleES (obj, entry, set);
89     }
90     
91     /** Getter of the data object that this support is associated with.
92     * @return data object passed in constructor
93     */

94     public final DataObject getDataObject () {
95         return obj;
96     }
97
98     /** Message to display when an object is being opened.
99     * @return the message or null if nothing should be displayed
100     */

101     protected String JavaDoc messageOpening () {
102         return NbBundle.getMessage (DataObject.class , "CTL_ObjectOpen", // NOI18N
103
obj.getPrimaryFile().getNameExt(),
104             FileUtil.getFileDisplayName(obj.getPrimaryFile())
105         );
106     }
107     
108
109     /** Message to display when an object has been opened.
110     * @return the message or null if nothing should be displayed
111     */

112     protected String JavaDoc messageOpened () {
113         return NbBundle.getMessage (DataObject.class, "CTL_ObjectOpened", // NOI18N
114
obj.getPrimaryFile().getNameExt(),
115             FileUtil.getFileDisplayName(obj.getPrimaryFile())
116         );
117     }
118
119     /** Constructs message that should be displayed when the data object
120     * is modified and is being closed.
121     *
122     * @return text to show to the user
123     */

124     protected String JavaDoc messageSave () {
125         return NbBundle.getMessage (
126             DataObject.class,
127             "MSG_SaveFile", // NOI18N
128
obj.getPrimaryFile().getNameExt()
129         );
130     }
131     
132     /** Constructs message that should be used to name the editor component.
133     *
134     * @return name of the editor
135     */

136     protected String JavaDoc messageName () {
137         if (! obj.isValid()) return ""; // NOI18N
138

139         return addFlagsToName(obj.getNodeDelegate().getDisplayName());
140     }
141     
142     protected String JavaDoc messageHtmlName() {
143         if (! obj.isValid()) return null;
144
145         String JavaDoc name = obj.getNodeDelegate().getHtmlDisplayName();
146         if (name != null) {
147             if (!name.startsWith("<html>")) {
148                 name = "<html>" + name;
149             }
150             name = addFlagsToName(name);
151         }
152         return name;
153     }
154         
155     /** Helper only. */
156     private String JavaDoc addFlagsToName(String JavaDoc name) {
157         int version = 3;
158         if (isModified ()) {
159             if (!obj.getPrimaryFile().canWrite()) {
160                 version = 2;
161             } else {
162                 version = 1;
163             }
164         } else {
165             if (!obj.getPrimaryFile().canWrite()) {
166                 version = 0;
167             }
168         }
169
170         return NbBundle.getMessage (DataObject.class, "LAB_EditorName",
171         new Integer JavaDoc (version), name );
172     }
173     
174     protected String JavaDoc documentID() {
175         if (! obj.isValid()) return ""; // NOI18N
176
return obj.getPrimaryFile().getName();
177     }
178
179     /** Text to use as tooltip for component.
180     *
181     * @return text to show to the user
182     */

183     protected String JavaDoc messageToolTip () {
184         // update tooltip
185
return FileUtil.getFileDisplayName(obj.getPrimaryFile());
186     }
187     
188     /** Computes display name for a line based on the
189      * name of the associated DataObject and the line number.
190      *
191      * @param line the line object to compute display name for
192      * @return display name for the line like "MyFile.java:243"
193      *
194      * @since 4.3
195      */

196     protected String JavaDoc messageLine (Line line) {
197         return NbBundle.getMessage(DataObject.class, "FMT_LineDisplayName2",
198             obj.getPrimaryFile().getNameExt(),
199             FileUtil.getFileDisplayName(obj.getPrimaryFile()),
200             new Integer JavaDoc(line.getLineNumber() + 1));
201     }
202     
203     
204     /** Annotates the editor with icon from the data object and also sets
205      * appropriate selected node. But only in the case the data object is valid.
206      * This implementation also listen to display name and icon chamges of the
207      * node and keeps editor top component up-to-date. If you override this
208      * method and not call super, please note that you will have to keep things
209      * synchronized yourself.
210      *
211      * @param editor the editor that has been created and should be annotated
212      */

213     protected void initializeCloneableEditor (CloneableEditor editor) {
214         // Prevention to bug similar to #17134. Don't call getNodeDelegate
215
// on invalid data object. Top component should be discarded later.
216
if(obj.isValid()) {
217             Node ourNode = obj.getNodeDelegate();
218             editor.setActivatedNodes (new Node[] { ourNode });
219             editor.setIcon(ourNode.getIcon (java.beans.BeanInfo.ICON_COLOR_16x16));
220             NodeListener nl = new DataNodeListener(editor);
221             ourNode.addNodeListener(org.openide.nodes.NodeOp.weakNodeListener (nl, ourNode));
222             nodeL = nl;
223         }
224     }
225
226     /** Called when closed all components. Overrides superclass method,
227      * also unregisters listening on node delegate. */

228     protected void notifyClosed() {
229         // #27645 All components were closed, unregister weak listener on node.
230
nodeL = null;
231         
232         super.notifyClosed();
233     }
234     
235     /** Let's the super method create the document and also annotates it
236     * with Title and StreamDescription properities.
237     *
238     * @param kit kit to user to create the document
239     * @return the document annotated by the properties
240     */

241     protected StyledDocument createStyledDocument (EditorKit kit) {
242         StyledDocument doc = super.createStyledDocument (kit);
243             
244         // set document name property
245
doc.putProperty(javax.swing.text.Document.TitleProperty,
246             FileUtil.getFileDisplayName(obj.getPrimaryFile())
247         );
248         // set dataobject to stream desc property
249
doc.putProperty(javax.swing.text.Document.StreamDescriptionProperty,
250             obj
251         );
252         
253         return doc;
254     }
255
256     /** Checks whether is possible to close support components.
257      * Overrides superclass method, adds checking
258      * for read-only property of saving file and warns user in that case. */

259     protected boolean canClose() {
260         if(desEnv().isModified() && isEnvReadOnly()) {
261             Object JavaDoc result = DialogDisplayer.getDefault().notify(
262                 new NotifyDescriptor.Confirmation(
263                     NbBundle.getMessage(DataObject.class,
264                         "MSG_FileReadOnlyClosing",
265                         new Object JavaDoc[] {((Env)env).getFileImpl().getNameExt()}),
266                     NotifyDescriptor.OK_CANCEL_OPTION,
267                     NotifyDescriptor.WARNING_MESSAGE
268             ));
269
270             return result == NotifyDescriptor.OK_OPTION;
271         }
272         
273         return super.canClose();
274     }
275
276     /** Saves document. Overrides superclass method, adds checking
277      * for read-only property of saving file and warns user in that case. */

278     public void saveDocument() throws IOException {
279         if(desEnv().isModified() && isEnvReadOnly()) {
280             IOException e = new IOException("File is read-only: " + ((Env)env).getFileImpl()); // NOI18N
281
UIException.annotateUser(e, null,
282                                      org.openide.util.NbBundle.getMessage(org.openide.loaders.DataObject.class,
283                                                                           "MSG_FileReadOnlySaving",
284                                                                           new java.lang.Object JavaDoc[]{((org.openide.text.DataEditorSupport.Env) env).getFileImpl().getNameExt()}),
285                                      null, null);
286             throw e;
287         }
288         super.saveDocument();
289     }
290
291     /** Indicates whether the <code>Env</code> is read only. */
292     boolean isEnvReadOnly() {
293         CloneableEditorSupport.Env env = desEnv();
294         return env instanceof Env && !((Env) env).getFileImpl().canWrite();
295     }
296     
297     /** Needed for EditorSupport */
298     final DataObject getDataObjectHack2 () {
299         return obj;
300     }
301     
302     /** Accessor for updateTitles.
303      */

304     final void callUpdateTitles () {
305         updateTitles ();
306     }
307     
308     /** Support method that extracts a DataObject from a Line. If the
309      * line is created by a DataEditorSupport then associated DataObject
310      * can be accessed by this method.
311      *
312      * @param l line object
313      * @return data object or null
314      *
315      * @since 4.3
316      */

317     public static DataObject findDataObject (Line l) {
318         if (l == null) throw new NullPointerException JavaDoc();
319         return (DataObject)l.getLookup ().lookup (DataObject.class);
320     }
321     
322     /** Environment that connects the data object and the CloneableEditorSupport.
323     */

324     public static abstract class Env extends OpenSupport.Env implements CloneableEditorSupport.Env {
325         /** generated Serialized Version UID */
326         static final long serialVersionUID = -2945098431098324441L;
327
328         /** The file object this environment is associated to.
329         * This file object can be changed by a call to refresh file.
330         */

331         private transient FileObject fileObject;
332
333         /** Lock acquired after the first modification and used in save.
334         * Transient => is not serialized.
335         * Not private for tests.
336         */

337         transient FileLock fileLock;
338         /** did we warned about the size of the file?
339          */

340         private transient boolean warned;
341
342         /** Constructor.
343         * @param obj this support should be associated with
344         */

345         public Env (DataObject obj) {
346             super (obj);
347         }
348         
349         /** Getter for the file to work on.
350         * @return the file
351         */

352         private FileObject getFileImpl () {
353             // updates the file if there was a change
354
changeFile();
355             return fileObject;
356         }
357         
358         /** Getter for file associated with this environment.
359         * @return the file input/output operation should be performed on
360         */

361         protected abstract FileObject getFile ();
362
363         /** Locks the file.
364         * @return the lock on the file getFile ()
365         * @exception IOException if the file cannot be locked
366         */

367         protected abstract FileLock takeLock () throws IOException;
368                 
369         /** Method that allows subclasses to notify this environment that
370         * the file associated with this support has changed and that
371         * the environment should listen on modifications of different
372         * file object.
373         */

374         protected final void changeFile () {
375
376             FileObject newFile = getFile ();
377             
378             if (newFile.equals (fileObject)) {
379                 // the file has not been updated
380
return;
381             }
382             
383             boolean lockAgain;
384             if (fileLock != null) {
385 // <> NB #61818 In case the lock was not active (isValid() == false), the new lock was taken,
386
// which seems to be incorrect. There is taken a lock on new file, while it there wasn't on the old one.
387
// fileLock.releaseLock ();
388
// lockAgain = true;
389
// =====
390
if(fileLock.isValid()) {
391                     ERR.fine("changeFile releaseLock: " + fileLock + " for " + fileObject); // NOI18N
392
fileLock.releaseLock ();
393                     lockAgain = true;
394                 } else {
395                     fileLock = null;
396                     lockAgain = false;
397                 }
398 // </>
399
} else {
400                 lockAgain = false;
401             }
402
403             fileObject = newFile;
404             ERR.fine("changeFile: " + newFile + " for " + fileObject); // NOI18N
405
fileObject.addFileChangeListener (new EnvListener (this));
406
407             if (lockAgain) { // refresh lock
408
try {
409                     fileLock = takeLock ();
410                     ERR.fine("changeFile takeLock: " + fileLock + " for " + fileObject); // NOI18N
411
} catch (IOException e) {
412                     Logger.getLogger(DataEditorSupport.class.getName()).log(Level.WARNING, null, e);
413                 }
414             }
415             
416         }
417         
418         
419         /** Obtains the input stream.
420         * @exception IOException if an I/O error occures
421         */

422         public InputStream inputStream() throws IOException {
423             final FileObject fo = getFileImpl ();
424             if (!warned && fo.getSize () > 1024 * 1024) {
425                 class ME extends org.openide.util.UserQuestionException {
426                     private long size;
427                     
428                     public ME (long size) {
429                         super ("The file is too big. " + size + " bytes.");
430                         this.size = size;
431                     }
432                     
433                     public String JavaDoc getLocalizedMessage () {
434                         Object JavaDoc[] arr = {
435                             fo.getPath (),
436                             fo.getNameExt (),
437                             new Long JavaDoc (size), // bytes
438
new Long JavaDoc (size / 1024 + 1), // kilobytes
439
new Long JavaDoc (size / (1024 * 1024)), // megabytes
440
new Long JavaDoc (size / (1024 * 1024 * 1024)), // gigabytes
441
};
442                         return NbBundle.getMessage(DataObject.class, "MSG_ObjectIsTooBig", arr);
443                     }
444                     
445                     public void confirmed () {
446                         warned = true;
447                     }
448                 }
449                 throw new ME (fo.getSize ());
450             }
451             InputStream is = getFileImpl ().getInputStream ();
452             return is;
453         }
454         
455         /** Obtains the output stream.
456         * @exception IOException if an I/O error occures
457         */

458         public OutputStream outputStream() throws IOException {
459             ERR.fine("outputStream: " + fileLock + " for " + fileObject); // NOI18N
460
if (fileLock == null || !fileLock.isValid()) {
461                 fileLock = takeLock ();
462             }
463             ERR.fine("outputStream after takeLock: " + fileLock + " for " + fileObject); // NOI18N
464
try {
465                 return getFileImpl ().getOutputStream (fileLock);
466             } catch (IOException fse) {
467             // [pnejedly] just retry once.
468
// Ugly workaround for #40552
469
if (fileLock == null || !fileLock.isValid()) {
470                     fileLock = takeLock ();
471                 }
472                 ERR.fine("ugly workaround for #40552: " + fileLock + " for " + fileObject); // NOI18N
473
return getFileImpl ().getOutputStream (fileLock);
474             }
475         }
476         
477         /** The time when the data has been modified
478         */

479         public Date getTime() {
480             // #32777 - refresh file object and return always the actual time
481
getFileImpl().refresh(false);
482             return getFileImpl ().lastModified ();
483         }
484         
485         /** Mime type of the document.
486         * @return the mime type to use for the document
487         */

488         public String JavaDoc getMimeType() {
489             return getFileImpl ().getMIMEType ();
490         }
491         
492         /** First of all tries to lock the primary file and
493         * if it succeeds it marks the data object modified.
494          * <p><b>Note: There is a contract (better saying a curse)
495          * that this method has to call {@link #takeLock} method
496          * in order to keep working some special filesystem's feature.
497          * See <a HREF="http://www.netbeans.org/issues/show_bug.cgi?id=28212">issue #28212</a></b>.
498         *
499         * @exception IOException if the environment cannot be marked modified
500         * (for example when the file is readonly), when such exception
501         * is the support should discard all previous changes
502          * @see org.openide.filesystems.FileObject#isReadOnly
503         */

504         public void markModified() throws java.io.IOException JavaDoc {
505             // XXX This shouldn't be here. But it is due to the 'contract',
506
// see javadoc to this method.
507
if (fileLock == null || !fileLock.isValid()) {
508                 fileLock = takeLock ();
509             }
510             ERR.fine("markModified: " + fileLock + " for " + fileObject); // NOI18N
511

512             if (!getFileImpl().canWrite()) {
513                 if(fileLock != null && fileLock.isValid()) {
514                     fileLock.releaseLock();
515                 }
516                 throw new IOException("File " // NOI18N
517
+ getFileImpl().getNameExt() + " is read-only!"); // NOI18N
518
}
519
520             this.getDataObject ().setModified (true);
521         }
522         
523         /** Reverse method that can be called to make the environment
524         * unmodified.
525         */

526         public void unmarkModified() {
527             ERR.fine("unmarkModified: " + fileLock + " for " + fileObject); // NOI18N
528
if (fileLock != null && fileLock.isValid()) {
529                 fileLock.releaseLock();
530                 ERR.fine("releaseLock: " + fileLock + " for " + fileObject); // NOI18N
531
}
532             
533             this.getDataObject ().setModified (false);
534         }
535         
536         /** Called from the EnvListener
537         * @param expected is the change expected
538         * @param time of the change
539         */

540         final void fileChanged (boolean expected, long time) {
541             ERR.fine("fileChanged: " + expected + " for " + fileObject); // NOI18N
542
if (expected) {
543                 // newValue = null means do not ask user whether to reload
544
firePropertyChange (PROP_TIME, null, null);
545             } else {
546                 firePropertyChange (PROP_TIME, null, new Date (time));
547             }
548         }
549
550         /** Called from the <code>EnvListener</code>.
551          * The components are going to be closed anyway and in case of
552          * modified document its asked before if to save the change. */

553         final void fileRemoved(boolean canBeVetoed) {
554             /* JST: Do not do anything here, as there will be new call from
555                the DataObject.markInvalid0
556              
557             if (canBeVetoed) {
558                 try {
559                     // Causes the 'Save' dialog to show if necessary.
560                     fireVetoableChange(Env.PROP_VALID, Boolean.TRUE, Boolean.FALSE);
561                 } catch(PropertyVetoException pve) {
562                     // ok vetoed, keep the window open, but continue to veto for ever
563                     // any subsequent veto messages from the data object
564                 }
565             }
566             
567             // Closes the components.
568             firePropertyChange(Env.PROP_VALID, Boolean.TRUE, Boolean.FALSE);
569              */

570         }
571         
572         public CloneableOpenSupport findCloneableOpenSupport() {
573             CloneableOpenSupport cos = super.findCloneableOpenSupport ();
574             if (cos instanceof DataEditorSupport) {
575                 Object JavaDoc o = ((DataEditorSupport)cos).env;
576                 if (o != this && o instanceof Env) {
577                    ((Env)o).warned = this.warned;
578                 }
579             }
580             return cos;
581         }
582         
583         private void readObject (ObjectInputStream ois) throws ClassNotFoundException JavaDoc, IOException {
584             ois.defaultReadObject ();
585             warned = true;
586         }
587     } // end of Env
588

589     /** Listener on file object that notifies the Env object
590     * that a file has been modified.
591     */

592     private static final class EnvListener extends FileChangeAdapter {
593         /** Reference (Env) */
594         private Reference JavaDoc<Env> env;
595         
596         /** @param env environement to use
597         */

598         public EnvListener (Env env) {
599             this.env = new java.lang.ref.WeakReference JavaDoc<Env> (env);
600         }
601
602
603         /** Handles <code>FileObject</code> deletion event. */
604         public void fileDeleted(FileEvent fe) {
605             Env env = this.env.get();
606             FileObject fo = fe.getFile();
607             if(env == null || env.getFileImpl() != fo) {
608                 // the Env change its file and we are not used
609
// listener anymore => remove itself from the list of listeners
610
fo.removeFileChangeListener(this);
611                 return;
612             }
613             
614             fo.removeFileChangeListener(this);
615             
616             env.fileRemoved(true);
617             fo.addFileChangeListener(this);
618         }
619         
620         /** Fired when a file is changed.
621         * @param fe the event describing context where action has taken place
622         */

623         public void fileChanged(FileEvent fe) {
624             Env env = this.env.get ();
625             if (env == null || env.getFileImpl () != fe.getFile ()) {
626                 // the Env change its file and we are not used
627
// listener anymore => remove itself from the list of listeners
628
fe.getFile ().removeFileChangeListener (this);
629                 return;
630             }
631
632             // #16403. Added handling for virtual property of the file.
633
if(fe.getFile().isVirtual()) {
634                 // Remove file event coming as consequence of this change.
635
fe.getFile().removeFileChangeListener(this);
636                 // File doesn't exist on disk -> simulate env is invalid,
637
// even the fileObject could be valid, see VCS FS.
638
env.fileRemoved(true);
639                 fe.getFile().addFileChangeListener(this);
640             } else {
641                 env.fileChanged (fe.isExpected (), fe.getTime ());
642             }
643         }
644                 
645     }
646     
647     /** Listener on node representing asociated data object, listens to the
648      * property changes of the node and updates state properly
649      */

650     private final class DataNodeListener extends NodeAdapter {
651         /** Asociated editor */
652         CloneableEditor editor;
653         
654         DataNodeListener (CloneableEditor editor) {
655             this.editor = editor;
656         }
657         
658         public void propertyChange(final PropertyChangeEvent ev) {
659             Mutex.EVENT.writeAccess(new Runnable JavaDoc() {
660                 public void run() {
661             if (Node.PROP_DISPLAY_NAME.equals(ev.getPropertyName())) {
662                 callUpdateTitles();
663             }
664             if (Node.PROP_ICON.equals(ev.getPropertyName())) {
665                 if (obj.isValid()) {
666                     editor.setIcon(obj.getNodeDelegate().getIcon (java.beans.BeanInfo.ICON_COLOR_16x16));
667                 }
668             }
669                 }
670             });
671         }
672         
673     } // end of DataNodeListener
674

675     /** Lookup that holds DataObject, its primary file and updates if that
676      * changes.
677      */

678     private static class DOEnvLookup extends AbstractLookup
679     implements PropertyChangeListener {
680         private DataObject dobj;
681         private InstanceContent ic;
682         
683         public DOEnvLookup (DataObject dobj) {
684             this (dobj, new InstanceContent ());
685         }
686         
687         private DOEnvLookup (DataObject dobj, InstanceContent ic) {
688             super (ic);
689             this.ic = ic;
690             this.dobj = dobj;
691             dobj.addPropertyChangeListener(WeakListeners.propertyChange(this, dobj));
692      
693             updateLookup ();
694         }
695         
696         private void updateLookup() {
697             ic.set(Arrays.asList(new Object JavaDoc[] { dobj, dobj.getPrimaryFile() }), null);
698         }
699         
700         public void propertyChange(PropertyChangeEvent ev) {
701             String JavaDoc propName = ev.getPropertyName();
702             if (propName == null || propName == DataObject.PROP_PRIMARY_FILE) {
703                 updateLookup();
704             }
705         }
706     }
707     
708 }
709
Popular Tags