This Bugzilla instance is a read-only archive of historic NetBeans bug reports. To report a bug in NetBeans please follow the project's instructions for reporting issues.

Bug 200698 - API Review: API to allow clients to attach source (javadoc) roots to binary roots.
Summary: API Review: API to allow clients to attach source (javadoc) roots to binary r...
Status: RESOLVED FIXED
Alias: None
Product: java
Classification: Unclassified
Component: Project (show other bugs)
Version: 7.1
Hardware: All All
: P2 normal (vote)
Assignee: Tomas Zezula
URL:
Keywords: API, API_REVIEW_FAST
: 160485 (view as bug list)
Depends on: 201043 202785 202894 204017
Blocks: 200910 202786 47498
  Show dependency tree
 
Reported: 2011-08-05 11:12 UTC by Tomas Zezula
Modified: 2011-10-20 14:42 UTC (History)
3 users (show)

See Also:
Issue Type: TASK
Exception Reporter:


Attachments
Diff file (122.59 KB, patch)
2011-08-05 11:12 UTC, Tomas Zezula
Details | Diff
Patch file (133.02 KB, patch)
2011-08-08 13:12 UTC, Tomas Zezula
Details | Diff
Patch file (168.71 KB, patch)
2011-08-09 19:31 UTC, Tomas Zezula
Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Tomas Zezula 2011-08-05 11:12:26 UTC
Created attachment 109813 [details]
Diff file

Added a new API and SPI to allow clients like java editor, code completion to attach the source roots and javadoc roots to binary roots. The SPI allows providers like maven, java platform, java libraries to handle the attaching in specific way. Also a default provider handling unknown binary roots was added, this provider stores bindings in IDE's user dir.

Currently there is no UI for attaching http javadoc. I am now working on it with Jano.
Comment 1 Tomas Zezula 2011-08-05 11:13:57 UTC
The sources are also available on branch javadoc_source_attacher in http://hg.netbeans.org/prototypes/ repository.
Comment 2 Jesse Glick 2011-08-05 13:42:03 UTC
[JG01] Calling SJAI from EQ does not seem like a good idea, because it must return a result code which may depend on a long-running operation. In particular the Maven implementation is expecting that it is _not_ called from EQ: it can block on the network operation, and until that completes there is no way whether it can know if the result was ATTACHED or CANCELED.

I think it is better to ask the SPI to be called from a background thread holding no locks. An impl which needs to show a dialog can do so using invokeAndWait.

Alternately, return a Future<Result> (SPI) or Future<Boolean> (API). (Future.get throws CancellationException which could substitute for the CANCELED result code, perhaps.) I am not sure which style is more comfortable for both API and SPI impls.
Comment 3 Tomas Zezula 2011-08-08 13:12:40 UTC
Created attachment 109858 [details]
Patch file
Comment 4 Tomas Zezula 2011-08-08 13:13:32 UTC
Fixed JG01. The SPI is now Future<Result> based.
Comment 5 Jesse Glick 2011-08-08 22:54:38 UTC
The impl of JG01 looks very strange to me. If the API continues to return an immediate value, what is the purpose of using Future in the SPI? It just makes the code much more complicated (SourceJavadocAttacherUtil) and serves no apparent purpose. AttachSourcePanel just posts the whole thing into a new RP; and ElementJavadoc.resolveLink looks like it is improperly blocking. Would be simpler to make the API and SPI both synchronous and just document that the calling thread may not be EQ and may not hold locks, so the SPI is permitted to do whatever it wants, incl. EQ.invokeAndWait.

If there is really some reason for the Future, then it should be done differently. The API should return Future<Boolean> and be documented not to block; the SPI should return Future<Boolean> if making an attempt to attach, or null for unsupported. (Currently the MavenSourceJavadocAttacher is written to produce a Future even when the two cheap checks for UNSUPPORTED are true, which seems wrong.) And CancellationException is not being used naturally because there is also a CANCELED status, which is confusing. Finally, the SPI methods still throw IOException, which makes no sense for something returning a Future.


If a synchronous API seems unacceptable, I would recommend getting rid of Future altogether; it does not seem to fit that well and is obviously very cumbersome to use. Instead, just define a callback:

interface AttachmentListener {
  void attachmentSucceeded();
  void attachmentFailed(); // or "canceled"
}

and then make the API:

/** return true if initiating attempt, false if unsupported */
public static boolean attachSources(@NonNull URL root, @NullAllowed AttachmentListener listener);

and the SPI to match (passed a no-op listener if necessary):

boolean attachSources(@NonNull URL root, @NonNull AttachmentListener listener);

Now the Maven impl would immediately return false in some cases; in others, it would return true after posting a job to a private RP, calling the listener at the end. DefaultSourceJavadocAttacher and others which show a dialog would return false if called on an unknown root; otherwise would return true after using EQ.invokeLater to show the dialog and call the listener.

Callers would either pass null for the listener if they do not need to refresh any visible display; or pass a listener which can update their display whenever it finishes.
Comment 6 Tomas Zezula 2011-08-09 07:58:01 UTC
>If the API continues to return an immediate value, what is the purpose of using Future in the SPI? 
Wrong.
The reason is to allow API client to call from any thread, not even from EDT thread.
The Future allows to reschedule to different thread from the caller thread.
API is called from RP but (SPI impl needs to show UI so it returns Future which is scheduled
into EDT). The client always needs the return value. So the API implementation hides the async
form the client.
>It just makesthe code much more complicated (SourceJavadocAttacherUtil) 
Which is not true. It just wraps the return into Future which is simple. I n fact the custom Future impls
are just for lazy evaluation and can be replaced by simple FutureTask.
>and serves no apparent purpose.
Also not true. It allows SPI to reshedule from caller thread to other one, like invokeAndWait.

>AttachSourcePanel just posts the whole thing into a new RP.
Right otherwise the maven will block the EDT.
>and ElementJavadoc.resolveLink looks like it is improperly blocking.
It's not yet rewritten.

>Would be simpler to make the API and SPI both synchronous and just document that the
>calling thread may not be EQ and may not hold locks, so the SPI is permitted to
>do whatever it wants, incl. EQ.invokeAndWait.
Future<Result> is better then EQ.invokeAndWait, the behavior is visible from signature. The Future<Result> does exactly what you described. I agree that it should be documented.

>If a synchronous API seems unacceptable, I would recommend getting rid of
>Future altogether; it does not seem to fit that well and is obviously very
>cumbersome to use. Instead, just define a callback:
The Future fits well and it's much better then the Listener approach which just moves
non needed asynchronous processing to client level and adds non needed type to API.

>Now the Maven impl would immediately return false in some cases; in others, it
>would return true after posting a job to a private RP, calling the listener at
>the end.
Which can be easily done even now with Future. Also Maven does not need to need
to use it's own private RP. If RP is needed you can just call run on FutureTask instead of
using listener.
Comment 7 Tomas Zezula 2011-08-09 08:38:08 UTC
Attaching the UI spec from Jano:

Attach Javadoc panel:

---
Attach Javadoc
---
Attach Javadoc with "lucene-core-2.9.3.jar:
(o) Remote Javadoc URL: |____________________|
( ) Local Javadoc:      |____________________| [ Browse... ]

[[ Attach ]] [ Cancel ]



Editor side bar:
Showing generated source file. No sources are attached to class' JAR file.   [ Attach Sources... ]

Attach sources dialog:
---
Attach Sources
---

Attach sources to /Users/.../.../lib/lucene-core-2.9.3.jar:
Sources: |____________________________________| [ Browse... ]

[[ Attach ]] [ Cancel ]
Comment 8 Tomas Mysik 2011-08-09 09:39:33 UTC
TM01: Is there a way to change the earlier provided Javadoc URL/location? Just wondering how to fix situation when incorrect URL/location is set.
Comment 9 Tomas Zezula 2011-08-09 09:55:51 UTC
TM01: Depends on the provider.
Currently for libraries provider - standard libraries UI, platform provider - standard platform UI, default - when source (javadoc) is not valid you still see the Attach button and customizer can be added into (Options/Miscellaneous). I don't know about Maven.
Comment 10 Jesse Glick 2011-08-09 13:12:51 UTC
(In reply to comment #6)
> the Listener approach just moves
> non needed asynchronous processing to client level

The client need not make any explicit mention of asynch processing. It is just the usual listener pattern:

if (SJA.attachSources(root, new SJA.Listener() {
  @Override void attached() {
    refreshSourcePane();
  }
  @Override void canceled() {
    EQ.invokeLater({=> attachButton.setVisible(false)}); // perhaps
  }
})) {
  attachButton.setEnabled(false);
}

(Similar to SwingWorker, AsyncHTTPRequest, etc.) The SPI is similarly easy to grasp and requires no subtle wrapper classes:

if (!isMyRoot()) {
  return false;
}
RP.post({=>
  try {
    download();
    listener.attached();
  } catch (DownloadInterruptedException x) {
    listener.canceled();
  }
});
return true;

> and adds non needed type to API.

Yes, but one with a simpler signature to understand and use than Future. Could even pass two @NullAllowed Runnable's if this is a concern:

if (SJA.attachSources(root, {=> refreshSourcePane()},
    {=> EQ.invokeLater({=> attachButton.setVisible(false)})) {
  attachButton.setEnabled(false);
}

(In reply to comment #9)
> TM01: ... I don't know about Maven.

In the case of the Maven provider, the user is not making any decisions so there is nothing to correct.
Comment 11 Tomas Zezula 2011-08-09 13:31:55 UTC
>if (!isMyRoot()) {
>  return false;
>}
>RP.post({=>
> try {
>    download();
>    listener.attached();
>  } catch (DownloadInterruptedException x) {
>    listener.canceled();
>  }
>});
>return true;

The Future version is just:
FutureTask ft;
if (isMyRoot()) {
  ft = new FutureTask ({=> return UNKNOWN;});
  ft.run();
} else {
  ft =  new FutureTask({=>
  try {
    download();
    return ATTACHED;
  } catch (DownloadInterruptedException x) {
    return CANCELED;
  }
  });
  RP.post(ft);
}
return ft;

For other providers (showing the UI is the same) except that RP.post() is replaced by SU.invokeLater().
I don't see much difference except that in Future version the client just calls the API in non EDT thread.
Anyway I don't have strong opinion about it.
Comment 12 Jesse Glick 2011-08-09 15:12:07 UTC
(In reply to comment #11)
> The Future version is just [...something short]

The current SourceJavadocAttacherUtil in the branch is rather lengthy and some of that nonobvious code is duplicated in the Maven impl. If this is mostly unnecessary, rewrite that code to show it can be short and simple (including an immediate UNSUPPORTED return from the Maven impl).

Also the current Javadoc says nothing about the API being blocking or when it could be called.
Comment 13 Tomas Zezula 2011-08-09 15:25:38 UTC
In fact the Now future is not needed. It's good only in case when lazy evaluation is needed which is not this case. Everything can be done with FutureTask as shown in comment #11.

>Also the current Javadoc says nothing about the API being blocking
It's two way operation. It has to be blocking for long running tasks.
>or when it could be called.
Yes, it's missing.
But if you prefer the Listener version it's not problem as I've written above I don't have strong opinion about it. I only did not agree that the Future version is much more complicated.
Comment 14 Tomas Zezula 2011-08-09 15:26:18 UTC
Changed UI according to UI spec: http://hg.netbeans.org/prototypes/rev/a768ce095c30
Comment 15 Tomas Zezula 2011-08-09 15:30:26 UTC
http://hg.netbeans.org/prototypes/rev/73569e593924
Comment 16 Tomas Zezula 2011-08-09 16:56:06 UTC
Here is a diff (rewrite of DefaultSourceJavadocAttacher to listeners).
It's not much less complicated then the Future version.

The Callable was replaced by Runnable.
return replaced by listener call

# This patch file was generated by NetBeans IDE
# It uses platform neutral UTF-8 encoding and \n newlines.
--- Base (BASE)
+++ Locally Modified (Based On LOCAL)
@@ -47,11 +47,12 @@
 import java.net.URI;
 import java.net.URL;
 import java.util.List;
-import java.util.concurrent.Callable;
-import java.util.concurrent.Future;
 import org.netbeans.api.annotations.common.NonNull;
+import org.netbeans.api.annotations.common.NullAllowed;
+import org.netbeans.api.java.queries.SourceJavadocAttacher.AttachmentListener;
 import org.netbeans.spi.java.queries.SourceJavadocAttacherImplementation;
 import org.openide.filesystems.FileStateInvalidException;
+import org.openide.util.Exceptions;
 import org.openide.util.NbBundle;
 import org.openide.util.lookup.ServiceProvider;
 
@@ -63,19 +64,28 @@
 public class DefaultSourceJavadocAttacher implements SourceJavadocAttacherImplementation {
 
     @Override
-    public Future<Result> attachSources(@NonNull final URL root) throws IOException {
-        return attach(root, 0);
+    public boolean attachSources(
+            @NonNull final URL root,
+            @NullAllowed final AttachmentListener listener) throws IOException {
+        return attach(root, listener, 0);
     }
 
     @Override
-    public Future<Result> attachJavadoc(@NonNull final URL root) throws IOException {
-        return attach(root, 1);
+    public boolean attachJavadoc(
+            @NonNull final URL root,
+            @NullAllowed final AttachmentListener listener) throws IOException {
+        return attach(root, listener, 1);
     }
 
-    private Future<Result> attach (@NonNull final URL root, final int mode) throws IOException {
-        final Callable<Result> call = new Callable<Result>() {
+    private boolean attach (
+            @NonNull final URL root,
+            @NullAllowed final AttachmentListener listener,
+            final int mode) throws IOException {
+        final Runnable call = new Runnable() {
             @Override
-            public Result call() throws Exception {
+            public void run() {
+                boolean success = false;
+                try {
                 final URL[] toAttach = selectRoots(root, mode);
                 if (toAttach != null) {
                     switch (mode) {
@@ -88,12 +98,18 @@
                         default:
                             throw new IllegalArgumentException(Integer.toString(mode));
                     }
-                    return Result.ATTACHED;
+                        success = true;
                 }
-                return Result.CANCELED;
+                } catch (MalformedURLException e) {
+                    Exceptions.printStackTrace(e);
+                } catch (FileStateInvalidException e) {
+                    Exceptions.printStackTrace(e);
             }
+                SourceJavadocAttacherUtil.callListener(listener,success);
+            }
         };
-        return SourceJavadocAttacherUtil.scheduleInEDT(call);
+        SourceJavadocAttacherUtil.scheduleInEDT(call);
+        return true;
     }
 
     @NbBundle.Messages({
Comment 17 Jesse Glick 2011-08-09 18:56:21 UTC
(In reply to comment #16)
> -        return SourceJavadocAttacherUtil.scheduleInEDT(call);
> +        SourceJavadocAttacherUtil.scheduleInEDT(call);

In which case SourceJavadocAttacherUtil.scheduleInEDT could just be deleted, because the return value is ignored, and you merely need EQ.invokeLater.

Stay with the Future version if you prefer it, just please simplify the impls somehow.
Comment 18 Tomas Zezula 2011-08-09 19:31:01 UTC
I've just rewritten it to use listeners. It was quite easy.
Jesse, can you review the Maven part. I don't know the Maven details so it may not be correct.
Comment 19 Tomas Zezula 2011-08-09 19:31:46 UTC
Created attachment 109893 [details]
Patch file
Comment 20 Tomas Zezula 2011-08-09 19:33:34 UTC
Here is the changeset rewriting Future to Listener
http://hg.netbeans.org/prototypes/rev/d18c7420de9f
Comment 21 Jesse Glick 2011-08-09 20:35:41 UTC
I did a bit of work in the branch, including changing the SPI to use a @NonNull listener. All appears to be working, though it would be nice if the Javadoc popup refreshed itself - or at least closed - when you click the attach link.
Comment 22 Tomas Zezula 2011-08-10 09:50:48 UTC
Thanks Jesse.
I've looked at changes and they seem fine.
I will try if it's possible to do the refresh in CC.
Comment 23 Tomas Zezula 2011-08-10 14:41:33 UTC
Code completion javadoc refreshed after attaching jdoc.
http://hg.netbeans.org/prototypes/rev/1e776157866f
Comment 24 Tomas Zezula 2011-08-10 15:31:21 UTC
I will integrate it tomorrow.
Comment 25 Tomas Zezula 2011-08-11 13:20:31 UTC
Fixed jet-main http://hg.netbeans.org/jet-main/rev/a01b7480064e
Comment 26 jn0101 2011-08-16 10:51:06 UTC
It doesen't work on NetBeans IDE Dev (Build 201108140601)
Java: 1.6.0_26; Java HotSpot(TM) Client VM 20.1-b02
System: Linux version 2.6.38-11-generic-pae running on i386; UTF-8; da_DK (nb)


I can confirm that there is now a message and an "Attach sources..." button at the top when viewing a class file where sources cannot be found.

However, using the button it seems I cannot actually attach the sources I want.

Ive tried 

1) A standard project, using json-1.0.jar. Using the "Attach sources..." and entering the path to the made no difference.

2) Opening 'Libraries', right clicking on json-1.0.jar there is a "Sources" field. Entering the path there DOES work and I can see the source.

During operation this came in the IDE log:

INFO [org.netbeans.modules.java.hints.WrongPackageSuggestion]: source cp is either null or does not contain the compiled source cp=
INFO [org.netbeans.modules.parsing.impl.TaskProcessor]: Task: class org.netbeans.modules.java.source.JavaSourceAccessor$CancelableTaskWrapper ignored cancel for 922 ms.
INFO [org.netbeans.modules.java.hints.WrongPackageSuggestion]: source cp is either null or does not contain the compiled source cp=
INFO [org.netbeans.modules.parsing.impl.TaskProcessor]: Task: class org.netbeans.modules.java.source.JavaSourceAccessor$CancelableTaskWrapper ignored cancel for 70 ms.
INFO [org.netbeans.modules.java.hints.WrongPackageSuggestion]: source cp is either null or does not contain the compiled source cp=
WARNING [org.netbeans.modules.java.source.tasklist.IncorrectErrorBadges]: Incorrect error badges detected, file=/home/j/slet/org/json/JSONObject.java.
WARNING [org.netbeans.modules.java.source.tasklist.IncorrectErrorBadges]: The file is not on its own source classpath, ignoring.
INFO [org.netbeans.modules.java.hints.WrongPackageSuggestion]: source cp is either null or does not contain the compiled source cp=
WARNING [org.netbeans.modules.java.source.parsing.JavacParser]: ClassPath identity changed for /home/j/.netbeans/dev/var/cache/index/s148/java/14/gensrc/org/json/JSONException.java@31bc4438:2ff366, class path owner: null original sourcePath: /home/j/Dropbox/APPbrain/Roskilde/Serverkommunikation/src new sourcePath: null






3) Using an Android project I was also unable to attach sources
(as I really want a fix for http://kenai.com/jira/browse/NBANDROID-71 - and on Android projects you cannot open 'Libraries' and right click on anything to attach sources)

Hovever, this extra error was logged:



INFO [org.netbeans.modules.parsing.impl.indexing.RepositoryUpdater]: Resolving dependencies took: 48 ms
INFO [org.netbeans.modules.parsing.impl.indexing.RepositoryUpdater]: Complete indexing of 0 binary roots took: 0 ms
INFO [org.netbeans.modules.project.libraries.Util]: Wrong resource bundle
Offending classloader: SystemClassLoader[405 modules]
Offending classloader: SystemClassLoader[405 modules]
Caused: java.util.MissingResourceException: No such bundle org.netbeans.modules.j2ee.sun.ide.j2ee.db.Bundle
	at org.openide.util.NbBundle.getBundle(NbBundle.java:451)
	at org.openide.util.NbBundle.getBundle(NbBundle.java:383)
[catch] at org.netbeans.modules.project.libraries.Util.getLocalizedString(Util.java:135)
	at org.netbeans.modules.project.libraries.Util.getLocalizedName(Util.java:88)
	at org.netbeans.modules.project.libraries.Util.getLocalizedName(Util.java:82)
	at org.netbeans.modules.project.libraries.ui.LibrariesModel$LibrariesComparator.compare(LibrariesModel.java:370)
	at org.netbeans.modules.project.libraries.ui.LibrariesModel$LibrariesComparator.compare(LibrariesModel.java:368)
	at java.util.TreeMap.put(TreeMap.java:530)
	at java.util.TreeSet.add(TreeSet.java:238)
	at org.netbeans.modules.project.libraries.ui.LibrariesModel.computeLibraries(LibrariesModel.java:322)
	at org.netbeans.modules.project.libraries.ui.LibrariesModel.access$100(LibrariesModel.java:84)
	at org.netbeans.modules.project.libraries.ui.LibrariesModel$2.run(LibrariesModel.java:299)
	at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:209)
	at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:641)
	at java.awt.EventQueue.access$000(EventQueue.java:84)
	at java.awt.EventQueue$1.run(EventQueue.java:602)
	at java.awt.EventQueue$1.run(EventQueue.java:600)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.security.AccessControlContext$1.doIntersectionPrivilege(AccessControlContext.java:87)
	at java.awt.EventQueue.dispatchEvent(EventQueue.java:611)
	at org.netbeans.core.TimableEventQueue.dispatchEvent(TimableEventQueue.java:148)
	at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:269)
	at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:184)
	at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:174)
	at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:169)
	at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:161)
	at java.awt.EventDispatchThread.run(EventDispatchThread.java:122)
ALL [null]: Offending classloader: SystemClassLoader[405 modules]
CONFIG [null]: Missing resource from class: org.netbeans.modules.j2ee.sun.ide.j2ee.db.Bundle
INFO [org.netbeans.modules.parsing.impl.indexing.RepositoryUpdater]: Complete indexing of 0 source roots took: 0 ms (New or modified files: 0, Deleted files: 0) [Adding listeners took: 0 ms]
WARNING [org.netbeans.modules.java.source.parsing.JavacParser]: ClassPath identity changed for /home/j/.netbeans/dev/var/cache/index/s143/java/14/gensrc/org/xmlpull/v1/XmlPullParser.java@9bd57205:3ea318, class path owner: null original sourcePath:  new sourcePath: null
Comment 27 Jesse Glick 2011-08-16 14:17:31 UTC
jn0101, your issues should be filed as separate bugs. Include steps to reproduce from scratch if possible.
Comment 28 jn0101 2011-08-17 07:38:12 UTC
It seems that after an IDE update and restart I can now attach sources to all JAR files I tried, except android.jar.

Thanks Jesse, I will file a seperate bug report.
Comment 29 jn0101 2011-08-17 09:31:09 UTC
(In reply to comment #28)
> It seems that after an IDE update and restart I can now attach sources to all
> JAR files I tried, except android.jar.

It seems that using a seperate --userdir makes all problems go away. 

So I think my problems was from interference of old NB 7.0 files, and probably not a bug, sorry.
Comment 30 Jesse Glick 2011-08-17 20:51:31 UTC
(In reply to comment #29)
> I think my problems was from interference of old NB 7.0 files, and probably
> not a bug

If some newly introduced IDE function fails to work due to the presence of old configuration in the userdir, especially if that configuration was created in the normal course of IDE usage, and especially if there is no correction made or warning issued by the IDE, then there is a bug. If you can continue to reproduce the problem using the old user directory then it should be possible to determine which preexisting file triggers it, using bisection.
Comment 31 Tomas Zezula 2011-08-25 07:51:04 UTC
I've found one problem. When the source (javadoc) is attached to library defined in the LibraryManager the IDE restart is needed. The reason of such a behavior is that there is already SourceForBinaryQuery.Result holding the library reference and the update of the library is done as delete + recreate (there is no updateLibrary in the API).
I am fixing it and adding updateLibrary method into LibraryManager.
Comment 32 Jesse Glick 2011-08-25 17:05:15 UTC
(In reply to comment #31)
> there is already SourceForBinaryQuery.Result holding the
> library reference and the update of the library is done as delete + recreate

If the SFBQ.R listened to changes in the set of libraries, it could react correctly even without any API change.
Comment 33 Tomas Zezula 2011-08-25 17:09:50 UTC
Yes, this is a part of the fix, the Results holds a LM reference and library name. The reference to Library is just a volatile cache.
But updateLibrary is also desired there is an RFE for it and delete + create may cause problems to other clients holding the Library ref.
Comment 34 Jesse Glick 2011-10-19 15:29:53 UTC
*** Bug 160485 has been marked as a duplicate of this bug. ***