NightOwl888 opened a new issue #446:
URL: https://github.com/apache/lucenenet/issues/446
We need to review exception types to ensure we are throwing and catching the
right exceptions.
## Why does it matter?
In Lucene, exceptions are used like glorified `goto` statements for control
flow by throwing exceptions in one place and catching them further up the
stack. While we made some good effort to remove this behavior from most of the
application, it is baked into the design of `IndexWriter`, `IndexReader` and
many types that support them. Removing the behaivor would require some major
reworking of the design and IMO would be too far removed from Lucene's design
to be maintainable.
## How it works in Lucene
Much like in .NET, Java exception families are controlled by inheritance.
However, there are certain "checked" exceptions that *must* be handled or
declared in the method signature for the caller to handle, and this behavior is
enforced by the compiler. So, to centralize places that handle common exception
scenarios, in Java it is convenient to throw an exception in many places and
handle that exception in a single place.
There are 4 main types of exceptions in Java and one more family that Lucene
cares about (`java.io.IOException`):
- `java.lang.Throwable` - Base class of all errors.
- `java.lang.Error` - indicates serious problems that a reasonable
application should not try to catch.
- `java.lang.Exception` - indicates conditions that a reasonable
application might want to catch.
- `java.lang.RuntimeException` - `RuntimeException` and its subclasses
are unchecked exceptions. Unchecked exceptions do not need to be declared in a
method or constructor's throws clause if they can be thrown by the execution of
the method or constructor and propagate outside the method or constructor
boundary.
- `java.io.IOException` - the general class of exceptions produced by
failed or interrupted I/O operations.
Basically, Java applications must handle everything that derive from
`java.lang.Exception` except for exceptions that derive from
`java.lang.RuntimeException`. As a result, a common thing to do throughout the
Lucene codebase is to catch `java.lang.Exception` (and all derived classes of
it) and wrap it in a `java.lang.RuntimeException` so it can be re-thrown. This
has a side-effect of bypassing all catch blocks and throwing the exception to
the outside world.
> **NOTE:** This bypass logic is hard to spot by the untrained eye. It is
tempting to take the catch blocks out and let the error propagate if not
considered carefully.
## How Lucene.NET is doing it
Since this is basically a line-by-line port, we use the closest exception
type we could match with the Java exception type. However:
1. We got it wrong in several cases.
2. We did it inconsistently - an exception type in one place wasn't
translated to the same exception type in another place.
3. We used `System.Exception` to replace `java.lang.Throwable`,
`java.lang.Error`, `java.lang.Exception`, and `java.lang.RuntimeException`
without considering all of the ramifications.
4. Java and .NET do not consistently derive from the same families of
exceptions, leaving several gaps when inheriting general exception types.
This means we are catching exceptions in cases where we should not, and
allowing exceptions to propagate that we should be catching and handling.
## Java Exceptions vs .NET Exceptions in Lucene
The following table is a listing of all exception types caught in Lucene
along with their inheritance hierarchy in both Java and in excpetions that
translated them to in .NET. Note that the .NET type list is not comprehensive,
but demonstrates that we aren't catching excpetions correctly because of the
errors we made above.
<details>
<summary>Click for details</summary>
<table>
<tr>
<th>Java Exception Type</th>
<th>Java Superclass Types</th>
<th>.NET Exception Type</th>
<th>.NET Superclass Types</th>
<th>Notes</th>
</tr>
<tr>
<td>java.io.IOException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception</td>
<td>System.IO.IOException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td></td>
</tr>
<tr>
<td rowspan="2">java.lang.ArrayIndexOutOfBoundsException</td>
<td rowspan="2">java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException <br/>--
java.lang.IndexOutOfBoundsException</td>
<td>System.IndexOutOfRangeException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td></td>
</tr>
<tr>
<td>System.ArgumentOutOfRangeException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException<br/>--- System.ArgumentException</td>
<td>We are currently not catching this exception in all places where
IndexOutOfRangeException is caught.</td>
</tr>
<tr>
<td rowspan="2">java.lang.StringIndexOutOfBoundsException</td>
<td rowspan="2">java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException <br/>--
java.lang.IndexOutOfBoundsException</td>
<td>System.IndexOutOfRangeException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td></td>
</tr>
<tr>
<td>System.ArgumentOutOfRangeException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException<br/>--- System.ArgumentException</td>
<td>We are currently not catching this exception in all places where
IndexOutOfRangeException is caught.</td>
</tr>
<tr>
<td rowspan="2">java.lang.IndexOutOfBoundsException</td>
<td rowspan="2">java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException</td>
<td>System.IndexOutOfRangeException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td></td>
</tr>
<tr>
<td>System.ArgumentOutOfRangeException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException<br/>--- System.ArgumentException</td>
<td>We are currently not catching this exception in all places where
IndexOutOfRangeException is caught.</td>
</tr>
<tr>
<td rowspan="3">java.text.ParseException</td>
<td rowspan="3">java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception</td>
<td>System.FormatException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td>Does not explicitly support a way to keep track of the error index,
but we could add an extension method and store the information in the
FormatExcption.Data property. Also, in QueryParser there were ParseException
classes that had been generated in Java - we might be able to eliminate these
and just use FormatException consistently.</td>
</tr>
<tr>
<td>System.InvalidOperationException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td>There are some places where ParseException errors were translated to
InvalidOperationException - these should probably be changed to
FormatException.</td>
</tr>
<tr>
<td>System.Exception</td>
<td>System.Object</td>
<td>There are some places where ParseException errors were translated to
Exception - these should probably be changed to FormatException.</td>
</tr>
<tr>
<td>java.lang.Exception</td>
<td>java.lang.Object <br/>- java.lang.Throwable</td>
<td>System.Exception</td>
<td>System.Object</td>
<td></td>
</tr>
<tr>
<td>java.lang.CloneNotSupportedException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception</td>
<td>None</td>
<td></td>
<td></td>
</tr>
<tr>
<td rowspan="2">java.io.FileNotFoundException</td>
<td rowspan="2">java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.io.IOException</td>
<td>System.IO.FileNotFoundException</td>
<td>System.Object <br/>- System.Exception <br/>-- System.SystemException
<br/>--- System.IO.IOException</td>
<td></td>
</tr>
<tr>
<td>System.IO.DirectoryNotFoundException</td>
<td>System.Object <br/>- System.Exception <br/>-- System.SystemException
<br/>--- System.IO.IOException</td>
<td></td>
</tr>
<tr>
<td>java.lang.NoSuchMethodException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.ReflectiveOperationException</td>
<td>System.MissingMethodException</td>
<td>System.Object <br/>- System.Exception <br/>-- System.SystemException
<br/>--- System.IO.IOException</td>
<td>This is wrong. Reflection in .NET doesn't throw exceptions when it
cannot find a method, it simply returns null. However, it can throw when there
is an ambiguous match, and not sure that is accounted for.</td>
</tr>
<tr>
<td>java.lang.reflect.InvocationTargetException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.ReflectiveOperationException</td>
<td>System.TargetInvocationException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.ApplicationException</td>
<td></td>
</tr>
<tr>
<td rowspan="2">java.lang.IllegalAccessException</td>
<td rowspan="2">java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.ReflectiveOperationException</td>
<td>System.Exception</td>
<td>System.Object <br/>- System.Exception</td>
<td>This seems to be done as a catch-all in .NET because Reflection
Invoke() can throw a number of different exceptions. But this needs further
review.</td>
</tr>
<tr>
<td>System.UnauthorizedAccessException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException<</td>
<td>This exception doesn't happen during Reflection calls, so this
should be changed to System.MemberAccessException, although partial trust is
obsolete so this error is likely only valid on .NET Framework.</td>
</tr>
<tr>
<td rowspan="3">java.lang.IllegalArgumentException</td>
<td rowspan="3">java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException</td>
<td>System.ArgumentException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td></td>
</tr>
<tr>
<td>System.ArgumentOutOfRangeException</td>
<td>System.Object <br/>- System.Exception <br/>-- System.SystemException
<br/>--- System.ArgumentException</td>
<td>Some guard clauses have been added with this type. Need to check to
ensure places that catch IndexOutOfRangeException and this exception are
correct.</td>
</tr>
<tr>
<td>System.ArgumentNullException</td>
<td>System.Object <br/>- System.Exception <br/>-- System.SystemException
<br/>--- System.ArgumentException</td>
<td>Some guard clauses have been added with this type (J2N). Since it
subclasses ArgumentException, we are covering IllegalArgumentException, but
need to review NullReferenceException to see if there are problems and perhaps
merge the two.</td>
</tr>
<tr>
<td>java.lang.NullPointerException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException</td>
<td>System.NullReferenceException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td>It might be best to convert everywhere this is used to
ArgumentNullException so we don't swallow NullReferenceException
unintentionally and hide problems.</td>
</tr>
<tr>
<td>java.lang.InstantiationException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.ReflectiveOperationException</td>
<td>System.TypeInitializationException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td>In Java, the exception comes from Reflection. In .NET, it may happen
due to a class initializer that throws an uncaught exception. These 2 are
definitely not the same thing, but I am not sure it makes a difference.</td>
</tr>
<tr>
<td>java.lang.UnsupportedOperationException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException</td>
<td>System.NotSupportedException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td></td>
</tr>
<tr>
<td>java.lang.Throwable</td>
<td>java.lang.Object</td>
<td>System.Exception</td>
<td>System.Object</td>
<td></td>
</tr>
<tr>
<td>java.lang.RuntimeException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception</td>
<td>System.Exception</td>
<td>System.Object</td>
<td></td>
</tr>
<tr>
<td>java.lang.ClassNotFoundException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.ReflectiveOperationException</td>
<td>None</td>
<td></td>
<td>Reflection in .NET has no equivalent. However, in places where it is
used we are not wrapping any exceptions in an Exception instance, which may
mean there could be problems with excpetions getting caught in calling code and
not handled appropriately.</td>
</tr>
<tr>
<td>java.util.NoSuchElementException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException</td>
<td>None</td>
<td></td>
<td>.NET enumerators do not throw when there are no more elements, they
return false. Some places are catching InvalidOperationException instead, but
they shouldn't be.</td>
</tr>
<tr>
<td>org.xml.sax.SAXException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception</td>
<td>Sax.SaxException</td>
<td>System.Object <br/>- System.Exception</td>
<td>Exists only in the Support folder of Lucene.Net.Benchmark.</td>
</tr>
<tr>
<td>java.lang.InterruptedException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception</td>
<td>System.Threading.ThreadInterruptedException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td></td>
</tr>
<tr>
<td>org.apache.lucene.util.ThreadInterruptedException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>-- System.SystemException</td>
<td>System.Threading.ThreadInterruptedException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td>This class exists to convert the exception into RuntimeException. It
is possible we have missed something fundamental here.</td>
</tr>
<tr>
<td>org.apache.lucene.benchmark.bytask.feeds.NoMoreDataException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception</td>
<td>Lucene.Net.Benchmark.ByTask.Feeds.NoMoreDataException</td>
<td>System.Object <br/>- System.Exception</td>
<td></td>
</tr>
<tr>
<td>java.lang.NumberFormatException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException <br/>----
java.lang.IllegalArgumentException</td>
<td>System.FormatException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td>It is possible we missed some catch blocks because FormatExcption
doesn't subclass ArgumentException, needs review to make sure the correct
exception types are being thrown.</td>
</tr>
<tr>
<td>org.apache.commons.compress.compressors.CompressorException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception</td>
<td>None</td>
<td></td>
<td>.NET and SharpZipLib do not throw when instantiating a compression
stream.</td>
</tr>
<tr>
<td>java.util.zip.DataFormatException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception</td>
<td>None</td>
<td></td>
<td>We aren't catching an exception for compression streams, but this
needs review.</td>
</tr>
<tr>
<td>java.lang.SecurityException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>-- java.lang.RuntimeException</td>
<td>System.Security.SecurityException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td></td>
</tr>
<tr>
<td rowspan="2">java.nio.file.NoSuchFileException</td>
<td rowspan="2">java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>-- java.io.IOException <br/>--
java.nio.file.FileSystemException</td>
<td>System.IO.FileNotFoundException</td>
<td>System.Object <br/>- System.Exception <br/>-- System.SystemException
<br/>--- System.IO.IOException</td>
<td></td>
</tr>
<tr>
<td>System.IO.DirectoryNotFoundException</td>
<td>System.Object <br/>- System.Exception <br/>-- System.SystemException
<br/>--- System.IO.IOException</td>
<td></td>
</tr>
<tr>
<td>org.apache.lucene.store.NoSuchDirectoryException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.io.IOException <br/>----
java.io.FileNotFoundDirectory</td>
<td>System.IO.DirectoryNotFoundException</td>
<td>System.Object <br/>- System.Exception <br/>-- System.SystemException
<br/>--- System.IO.IOException</td>
<td></td>
</tr>
<tr>
<td>java.lang.OutOfMemoryError</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>-- java.lang.Error
<br/>--- java.lang.VirtualMachineError</td>
<td>System.OutOfMemoryException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td>Microsoft recommends calling Environment.FailFast() in this case,
but we need a review to ensure we are doing so. I believe some tests are also
marked [Ignore] because it was assumed that OutOfMemoryException cannot be
caught in .NET.</td>
</tr>
<tr>
<td>org.apache.lucene.store.AlreadyClosedException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException <br/>---
java.lang.IllegalStateException</td>
<td>System.ObjectDisposedException</td>
<td>System.Object <br/>- System.Exception <br/>-- System.SystemException
<br/>-- System.InvalidOperationException</td>
<td>In prior versions of Lucene.NET, there were at least attempts to use
both ICloseable and IDisposable. We need to revisit this.</td>
</tr>
<tr>
<td>java.nio.charset.CharacterCodingException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.io.IOException</td>
<td>All</td>
<td></td>
<td>Perhaps catching all exceptions is not the right decision here.</td>
</tr>
<tr>
<td>org.apache.lucene.util.BytesRefHash.MaxBytesLengthExceededException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException</td>
<td>Lucene.Net.Util.BytesRefHash.MaxBytesLengthExceededException</td>
<td>System.Object <br/>- System.Exception</td>
<td></td>
</tr>
<tr>
<td>org.apache.lucene.search.CollectionTerminatedException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException</td>
<td>Lucene.Net.Search.CollectionTerminatedException</td>
<td>System.Object <br/>- System.Exception</td>
<td></td>
</tr>
<tr>
<td>java.util.concurrent.ExecutionException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception</td>
<td>All</td>
<td></td>
<td>Needs review.</td>
</tr>
<tr>
<td>java.nio.BufferUnderflowException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException</td>
<td>J2N.IO.BufferUnderflowException</td>
<td>System.Object <br/>- System.Exception</td>
<td></td>
</tr>
<tr>
<td>java.lang.ClassCastException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException</td>
<td>J2N.InvalidCastException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td></td>
</tr>
<tr>
<td>org.apache.lucene.store.LockObtainFailedException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.io.IOException</td>
<td>Lucene.Net.Store.LockObtainFailedException</td>
<td>System.Object <br/>- System.Exception <br/>-- System.SystemException
<br/>--- System.IO.IOException</td>
<td></td>
</tr>
<tr>
<td>java.security.PrivilegedActionException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception</td>
<td>None</td>
<td></td>
<td>This exception is used to work around a bug in Java in
MMapDirectory.</td>
</tr>
<tr>
<td>java.nio.channels.OverlappingFileLockException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException <br/>----
java.lang.IllegalStateException</td>
<td>None</td>
<td></td>
<td>NativeFSLockFactory was redesigned for .NET and this exception was
factored out.</td>
</tr>
<tr>
<td rowspan="2">java.lang.AssertionError</td>
<td rowspan="2">java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Error</td>
<td>System.InvalidOperationException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td>Lucene.Net.Diagnostics.AssertionException was added later. Should we
migrate everything over to it?</td>
</tr>
<tr>
<td>Lucene.Net.Diagnostics.AssertionException</td>
<td>System.Object <br/>- System.Exception</td>
<td>This is only used in Debugging.Assert() method calls.</td>
</tr>
<tr>
<td>java.lang.Error</td>
<td>java.lang.Object <br/>- java.lang.Throwable</td>
<td>System.Exception</td>
<td>System.Object</td>
<td></td>
</tr>
<tr>
<td>java.io.EOFException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>-- java.io.IOException</td>
<td>System.IO.EndOfStreamException</td>
<td>System.Object <br/>- System.Exception <br/>-- System.SystemException
<br/>--- System.IO.IOException</td>
<td></td>
</tr>
<tr>
<td>java.lang.IllegalStateException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException</td>
<td>System.InvalidOperationException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td></td>
</tr>
<tr>
<td>org.apache.lucene.index.IndexFormatTooOldException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.io.IOException <br/>---
org.apache.lucene.index.CorruptIndexException</td>
<td>Lucene.Net.Index.IndexFormatTooOldException</td>
<td>System.Object <br/>- System.Exception <br/>-- System.SystemException
<br/>-- System.IO.IOException</td>
<td></td>
</tr>
<tr>
<td>org.apache.lucene.index.CorruptIndexException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.io.IOException <br/>---
org.apache.lucene.index.CorruptIndexException</td>
<td>Lucene.Net.Index.IndexFormatTooOldException</td>
<td>System.Object <br/>- System.Exception <br/>-- System.SystemException
<br/>-- System.IO.IOException</td>
<td></td>
</tr>
<tr>
<td>org.apache.lucene.index.TestCrashCausesCorruptIndex.CrashingException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException</td>
<td>Lucene.Net.Index.TestCrashCausesCorruptIndex.CrashingException</td>
<td>System.Object <br/>- System.Exception</td>
<td>Only exists in one test class.</td>
</tr>
<tr>
<td>org.apache.lucene.util.SetOnce.AlreadySetException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException <br/>----
java.lang.IllegalStateException</td>
<td>Lucene.Net.Util.AlreadySetException</td>
<td>System.Object <br/>- System.Exception <br/>-- System.SystemException
<br/>-- System.InvalidOperationException</td>
<td></td>
</tr>
<tr>
<td>org.apache.lucene.store.MockDirectoryWrapper.FakeIOException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.io.IOException</td>
<td>Lucene.Net.Store.FakeIOException</td>
<td>System.Object <br/>- System.Exception <br/>-- System.SystemException
<br/>-- System.IO.IOException</td>
<td></td>
</tr>
<tr>
<td>org.apache.lucene.search.TimeLimitingCollector.TimeExceededException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException</td>
<td>Lucene.Net.Search.TimeLimitingCollector.TimeExceededException</td>
<td>System.Object <br/>- System.Exception</td>
<td></td>
</tr>
<tr>
<td>org.antlr.runtime.RecognitionException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception</td>
<td>Antlr.Runtime.RecognitionException</td>
<td>System.Object <br/>- System.Exception</td>
<td></td>
</tr>
<tr>
<td>java.lang.StackOverflowError</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>-- java.lang.Error
<br/>--- java.lang.VirtualMachineError</td>
<td>System.StackOverflowException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td>Uncatchable in .NET. Microsoft recommends breaking out of recursive
calls with a max value setting.</td>
</tr>
<tr>
<td>java.lang.ArithmeticException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException</td>
<td>System.ArithmeticException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td></td>
</tr>
<tr>
<td>org.apache.lucene.search.BooleanQuery.TooManyClauses</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException</td>
<td>Lucene.Net.Search.BooleanQuery.TooManyClausesException</td>
<td>System.Object <br/>- System.Exception</td>
<td></td>
</tr>
<tr>
<td>org.apache.lucene.queryparser.classic.ParseException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception</td>
<td>Lucene.Net.QueryParsers.Classic.ParseException</td>
<td>System.Object <br/>- System.Exception</td>
<td></td>
</tr>
<tr>
<td>org.apache.lucene.queryparser.classic.TokenMgrError</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Error</td>
<td>Lucene.Net.QueryParsers.Classic.TokenMgrError</td>
<td>System.Object <br/>- System.Exception</td>
<td></td>
</tr>
<tr>
<td>java.util.MissingResourceException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException</td>
<td>System.Resources.MissingManifestResourceException </td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td></td>
</tr>
<tr>
<td>org.apache.lucene.queryparser.flexible.standard.parser.ParseException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>---
org.apache.lucene.queryparser.flexible.core.QueryNodeException <br/>----
org.apache.lucene.queryparser.flexible.core.QueryNodeParseException</td>
<td>Lucene.Net.QueryParsers.Flexible.Standard.Parser.ParseException</td>
<td>System.Object <br/>- System.Exception <br/>--
Lucene.Net.QueryParsers.Flexible.Core.QueryNodeException <br/>---
Lucene.Net.QueryParsers.Flexible.Core.QueryNodeParseException</td>
<td></td>
</tr>
<tr>
<td>org.apache.lucene.queryparser.flexible.standard.parser.ParseException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception</td>
<td>Lucene.Net.QueryParsers.Flexible.Standard.Parser.ParseException</td>
<td>System.Object <br/>- System.Exception <br/>--
Lucene.Net.QueryParsers.Flexible.Core.QueryNodeException</td>
<td></td>
</tr>
<tr>
<td>org.apache.lucene.queryparser.flexible.standard.parser.TokenMgrError</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Error</td>
<td>Lucene.Net.QueryParsers.Flexible.Standard.Parser.TokenMgrError</td>
<td>System.Object <br/>- System.Exception</td>
<td></td>
</tr>
<tr>
<td>org.apache.lucene.queryparser.surround.parser.TokenMgrError</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Error</td>
<td>Lucene.Net.QueryParsers.Surround.Parser.TokenMgrError</td>
<td>System.Object <br/>- System.Exception</td>
<td></td>
</tr>
<tr>
<td>org.apache.lucene.queryparser.surround.ParseException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception</td>
<td>Lucene.Net.QueryParsers.Surround.ParseException</td>
<td>System.Object <br/>- System.Exception</td>
<td></td>
</tr>
<tr>
<td>org.apache.lucene.queryparser.xml.ParserException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception</td>
<td>Lucene.Net.QueryParsers.Xml.ParserException</td>
<td>System.Object <br/>- System.Exception</td>
<td></td>
</tr>
<tr>
<td>org.apache.lucene.replicator.SessionExpiredException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.io.IOException</td>
<td>Lucene.Net.Replicator.SessionExpiredException</td>
<td>System.Object <br/>- System.Exception <br/>-- System.SystemException
<br/>-- System.IO.IOException</td>
<td></td>
</tr>
<tr>
<td>java.lang.NoClassDefFoundError</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>-- java.lang.Error
<br/>--- java.lang.LinkageError</td>
<td>System.TypeLoadException</td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td>Review is required to determine if this is the correct exception in
.NET.</td>
</tr>
<tr>
<td>org.junit.AssumptionViolatedException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.lang.RuntimeException <br/>----
org.junit.internal.AssumptionViolatedException</td>
<td>NUnit.Framework.InconclusiveException</td>
<td>System.Object <br/>- System.Exception <br/>--
NUnit.Framework.ResultStateException</td>
<td></td>
</tr>
<tr>
<td>java.nio.file.AccessDeniedException</td>
<td>java.lang.Object <br/>- java.lang.Throwable <br/>--
java.lang.Exception <br/>--- java.io.IOException <br/>----
java.nio.file.FileSystemException</td>
<td>System.UnauthorizedAccessException </td>
<td>System.Object <br/>- System.Exception <br/>--
System.SystemException</td>
<td>Special case: Does not subclass IOException in .NET but does in
Java.</td>
</tr>
</table>
</details>
There is a lot of info in the above table, so let me just pick out a few
cases to demonstrate the kind of issues we are facing.
| Exception Type | Issue |
|---|---|
| `java.text.ParseException` | This excpetion was translated to
`FormatException` in some places, `InvalidOperationException` in some places,
and `Excpetion` in other places (there may be more). |
| `java.nio.file.AccessDeniedException` | The corresponding class in .NET,
`System.UnauthorizedAccessException` does not subclass `IOException`. As a
result, catch blocks for `IOException` are missing this one when they should be
catching it. |
| `java.io.FileNotFoundException` and `java.nio.file.NoSuchFileException` |
In .NET there is only 1 exception type for dealing with missing files. However,
in Java these exceptions are also thrown if there is a missing directory. |
| `org.apache.lucene.store.NoSuchDirectoryException` | This class was
excluded from Lucene.NET because .NET already has a
`System.IO.DirectoryNotFoundException`, however it does not subclass
`System.IO.FileNotFoundException`, so we needed to manually duplicate `catch`
blocks everywhere `System.IO.FileNotFoundExeption` was caught and duplicate
error handler logic. |
| `java.lang.OutOfMemoryError` | This excpetion derives from
`java.lang.Error`. This means in .NET when we catch `Exception`, we are
catching this exception when we shouldn't be in many cases and not allowing it
to propagate to its intended handler. |
| `java.lang.AssertionError` | Since in .NET assertions are compiled out of
the release and the behavior of `Debug.Assert()` doesn't consistently throw an
exception that can be caught, an equivalent for this exception wasn't added
until recently and is not in use in every place it was in Lucene. However, it
is also meant to be excluded from exception handling and propagate to the top
of the stack, but wouldn't be if we were using it for the same reason as
`java.lang.OutOfMemoryError`. |
| `java.lang.NullPointerException` | Since in Java the approach is to let
exceptions fly and handle them later, this exception is being caught in Lucene.
However, in most cases when there is a `null` parameter in .NET, the expected
exception is `ArgumentNullException` rather than `NullReferenceException`.
`NullReferenceException` is an unexpected exception in .NET we would expect to
let propagate so we can prevent it from being thrown more easily.
`NullReferenceException` was used in a lot of catch blocks, but this may be
hiding problems in the code where exceptions can be avoided. |
## Challenges
- Fill gaps between Java and .NET exception types in a way that doesn't rely
100% on inheritance, since we have several special cases to deal with in
mulitple places in the application
- Avoid exception translation problems when porting from Java to .NET
- Make it possible to maintain this easily - if we discover a rule about
Java exceptions we missed, it should be possible to add that rule in only 1
place
- Make the code mostly or completely self-documenting so we don't always
have to refer to documentation in order to translate exception types right
## Proposed Solution for Catching Exceptions
We can take advantage of the C# [`when` in a `catch`
statement](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/when#when-in-a-catch-statement)
to make complex rules in catch blocks. In addition, we can use extension
methods on `Exception` with names that match exception groups in Java to apply
the rules specifc to that group.
### Example
```c#
public static class ExceptionExtensions
{
public static bool IsThrowable(this Exception e)
{
return true; // All errors inherit from Throwable in Java
}
public static bool IsError(this Exception e)
{
return e is OutOfMemoryException ||
e is AssertionException ||
e is StackOverflowException;
}
public static bool IsException(this Exception e)
{
return e is Exception && !IsError(e);
}
public static bool IsIOException(this Exception e)
{
return e is IOException || e is UnauthorizedAccessException;
}
public static bool IsFileNotFoundOrNoSuchFileExcetption(this Exception e)
{
return e is FileNotFoundException || e is DirectoryNotFoundException;
}
}
```
#### FileNotFoundException Usage
<details>
<summary>Click to expand</summary>
##### Java
```java
public static boolean slowFileExists(Directory dir, String fileName) throws
IOException {
try {
dir.openInput(fileName, IOContext.DEFAULT).close();
return true;
} catch (NoSuchFileException | FileNotFoundException e) {
return false;
}
}
```
##### C# Before
```c#
public static bool SlowFileExists(Directory dir, string fileName)
{
try
{
dir.OpenInput(fileName, IOContext.DEFAULT).Dispose();
return true;
}
catch (FileNotFoundException)
{
return false;
}
// LUCENENET specific - .NET (thankfully) only has one
FileNotFoundException, so we don't need this
//catch (NoSuchFileException)
//{
// return false;
//}
// LUCENENET specific - since NoSuchDirectoryException subclasses
FileNotFoundException
// in Lucene, we need to catch it here to be on the safe side.
catch (DirectoryNotFoundException)
{
return false;
}
}
```
##### C# After
```c#
public static bool SlowFileExists(Directory dir, string fileName)
{
try
{
dir.OpenInput(fileName, IOContext.DEFAULT).Dispose();
return true;
}
catch (Exception e) when (e.IsFileNotFoundOrNoSuchFileExcetption())
{
return false;
}
}
```
</details>
#### IOException Usage
<details>
<summary>Click to expand</summary>
##### Java
```java
try {
TestUtil.rm(everything);
} catch (IOException e) {
Class<?> suiteClass = RandomizedContext.current().getTargetClass();
if (suiteClass.isAnnotationPresent(SuppressTempFileChecks.class)) {
System.err.println("WARNING: Leftover undeleted temporary files (bugUrl:
"
+ suiteClass.getAnnotation(SuppressTempFileChecks.class).bugUrl() +
"): "
+ e.getMessage());
return;
}
throw e;
}
```
##### C# Before
```c#
try
{
TestUtil.Rm(everything);
}
// LUCENENET specific: UnauthorizedAccessException doesn't subclass
IOException as
// AccessDeniedException does in Java, so we need a special case for it.
catch (UnauthorizedAccessException e)
{
// Type suiteClass =
RandomizedContext.Current.GetTargetType;
// if
(suiteClass.IsAnnotationPresent(typeof(SuppressTempFileChecks)))
// {
Console.Error.WriteLine("WARNING: Leftover undeleted temporary files " +
e.Message);
return;
// }
}
catch (IOException e)
{
// Type suiteClass =
RandomizedContext.Current.GetTargetType;
// if
(suiteClass.IsAnnotationPresent(typeof(SuppressTempFileChecks)))
// {
Console.Error.WriteLine("WARNING: Leftover undeleted temporary files " +
e.Message);
return;
// }
}
```
##### C# After
```c#
try
{
TestUtil.Rm(everything);
}
catch (Exception e) when (e.IsIOException())
{
// Type suiteClass =
RandomizedContext.Current.GetTargetType;
// if
(suiteClass.IsAnnotationPresent(typeof(SuppressTempFileChecks)))
// {
Console.Error.WriteLine("WARNING: Leftover undeleted temporary files " +
e.Message);
return;
// }
}
```
</details>
> **NOTE:** There are dozens of places where we catch `IOExcpetion` that
don't take `UnauthorizedAccessException` into account.
>
> Also, there may be other `IOExcption`-derived types in Java that don't
derive from `IOExcpetion` in .NET. We need to review all of the [direct known
subclasses](https://docs.oracle.com/javase/7/docs/api/java/io/IOException.html)
to be relatively sure we are catching everything that is supposed to be caught.
## Proposed Solution for Throwing Exceptions
Subclass .NET exceptions with internal classes that use Java exception
names. This takes the guesswork out of deciding which excpetion is the
appropriate one to throw in most cases, and we still throw the correct .NET
exception types (that can be caught) to the outside world.
There are several exceptions in Java where in .NET we don't get an
exception, but may get a `false` or `null` return value instead. We can create
fake excpetions for these and mark them with the `[Obsolete]` attribute with a
message indicating how to convert the .NET code.
### Example
```c#
internal class ParseException : FormatException
{
private const string ERROR_OFFSET_NAME = "__errorOffset";
public ParseException() {}
public ParseException(string message) : base(message) {}
public ParseException(string message, Exception innerException) :
base(message, innerException) {}
public int ErrorOffset
{
get => Convert.ToInt32(Data[ERROR_OFFSET_NAME] ?? 0); // Data is
passed on in FormatException so it is accessible even though this class is
internal
set => Data[ERROR_OFFSET_NAME] = value;
}
public override Message => base.Message + ", line: " + ErrorOffset;
}
[Obsolete("This exception is thrown by Reflection code in Java when calling
private/internal members of a Type, but is unnecessary in .NET.")]
internal class PrivilegedActionException : Exception
{
}
[Obsolete(".NET enumerators do not throw when there are no more elements. It
is recommended to convert iterators into enumerators and use a MoveNext()
method that returns false.")]
internal class NoSuchElementException: Exception
{
}
[Obsolete("Reflection doesn't throw exceptions when it cannot locate a type
in .NET, instead it will return null. Do not catch this exception and adjust
the logic to consider a null return value the equivalent of the error
condition.")]
internal class ClassNotFoundException : Exception
{
}
```
#### ParseException Usage
<details>
<summary>Click to expand</summary>
##### Java
```java
if (line.startsWith(KEEPCASE_KEY)) {
String parts[] = line.split("\\s+");
if (parts.length != 2) {
throw new ParseException("Illegal KEEPCASE declaration",
reader.getLineNumber());
}
keepcase = flagParsingStrategy.parseFlag(parts[1]);
}
```
##### C# Before
```c#
if (line.StartsWith(KEEPCASE_KEY, StringComparison.Ordinal))
{
string[] parts = whitespacePattern.Split(line).TrimEnd();
if (parts.Length != 2)
{
throw new FormatException(string.Format("Illegal KEEPCASE
declaration, line {0}", lineNumber));
}
keepcase = flagParsingStrategy.ParseFlag(parts[1]);
}
```
##### C# After
```c#
if (line.StartsWith(KEEPCASE_KEY, StringComparison.Ordinal))
{
string[] parts = whitespacePattern.Split(line).TrimEnd();
if (parts.Length != 2)
{
throw new ParseException("Illegal KEEPCASE declaration") {
ErrorOffset = lineNumber };
}
keepcase = flagParsingStrategy.ParseFlag(parts[1]);
}
```
</details>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
For queries about this service, please contact Infrastructure at:
[email protected]