Newer
Older
labs / tiddlers / content / labs / lab06 / $__Labs_06_Encapsulating Exceptions.md
If you haven't attended/reviewed lecture 10 yet then do so now --- this section will make a lot more sense if you understand why we need to encapsulate the JDBI and H2 exceptions in a way that hides them from the GUI classes.

Since we have gone to the effort of hiding all of the details relating to storage and persistence away from the user interface (by using DAO classes) it would be a shame if we suddenly had to start catching JDBI or H2 exceptions in our user interface.  If we did this and then decided to switch to a different form of data storage then we would need to rewrite all of the GUI code that used JDBI specific exceptions.

If the DAO was a class that had method implementations then we could catch any JDBI exceptions inside those methods, and then convert them into an exception class that we control such as a `DaoException`.  Since we control this class it is OK for us to use it in our GUI classes.  If we changed the data storage mechanism then we just need to make sure that we also convert the exceptions in the new DAO class to `DaoException` and our GUI classes don't need to be touched.

Since we are using the declarative API for JDBI where we only have an interface, it makes it a bit trickier to avoid directly using JDBI exceptions in the GUI classes since we have no place to catch the JDBI exceptions in the DAO class.

Intsead, we can create a 'mapper' class that converts exceptions into error messages.  The mapper class will use the JDBI and H2 exceptions to generate useful error messages which means that the GUI classes don't have to do it.  Basically, the mapper class will be the single point where we have to reference specific exceptions --- if we switch to a different storage mechanism then this is the only place that we need to change code to deal with the exceptions --- we have encapsulated the JDBI and H2 exceptions.

1.  Create a new class named `DaoExceptionMapper` in the `dao` package.

1.  Add the following code to the class:

    ```java
	private final Logger logger = LoggerFactory.getLogger(DaoExceptionMapper.class);

	public String messageFromException(Exception exception) {

		// is the error a constraint violation?
		if (exception.getCause() instanceof JdbcSQLIntegrityConstraintViolationException) {

			JdbcSQLIntegrityConstraintViolationException ex =
                    (JdbcSQLIntegrityConstraintViolationException) exception.getCause();
			
			// is it specifically a unique constraint violation?
			if (ex.getErrorCode() == 23505) {
				return "That ID is already in use.";
			
			// some other form of constraint error - these should have been prevented
            // via OVal, so we should consider this to be a bug
			} else {

				// log the error (since it is a bug that needs to be fixed)
				logger.error("DB constraint error", exception);
				
				// extract the column name from the message
				String column = exception.getMessage()
                      .replaceAll("(?s).*column \\\"(.*?)\\\";.*", "$1");

				return "The " + column + " field contains invalid data.";
			}
			
		// is it a connection failure?
		} else if (exception.getCause() instanceof SQLTransientConnectionException
                || exception.getCause() instanceof JdbcSQLNonTransientConnectionException) {
			
			// possibly a bug, so log it
			logger.error("DB connection error", exception);

			return "The application could not connect to the database.\n\nPlease verify that the database server is running.";

		// some unknown error - most likely a bug, so log it
		} else {
			logger.error("Unrecognised error while accessing DB", exception);

			return "An unrecognised error occurred when accessing the database.";
		}
	}
    ```

    The imports for `Logger` and `LoggerFactory` should come from the `org.slf4j` package.

    Error code `23505` is the H2 code for a unique constraint violation --- if we see that error then it pretty much always means that the user has entered the ID for an existing domain object, so we can return a message telling them exactly that.  These error codes are only semi-standarised --- codes in the 23000 range are generally used for constraint violations by most DBMSs (except Oracle which stubbornly does things its own way as usual).  Each DBMS has its own numbers for specific forms of constraint violation, so we can't really generalise this code.  The `JdbcSQLNonTransientConnectionException` and `JdbcSQLIntegrityConstraintViolationException` exceptions are specific to H2.  The `SQLTransientConnectionException` is part of JDBC.

    Since we are using OVal to prevent most of the problems that are likely to occur due to the user entering bad data, if we see any other form of constraint error then that suggests that our OVal constraints are not sufficient --- you should add more OVal constraints to prevent bad data from being sent to the database.  The remaining code is relating to problems that are most likely due to bugs, so we log the exceptions so that developers can use the stack traces to debug the problem.  Currently the logger is only sending the logs to the output pane, but we could easily configure it to write the logs to a file, or send them over the internet to a log server that the developers are monitoring.

    In case you are wondering what the gibberish that looks like a cat walked over the keyboard in the `replaceAll` call is about --- that is what is known a  *regular expression* which is a query language for working with text.  Regular expressions can be pretty impenetrable, but are useful to know if you need to extract specific bits and pieces out of text which is what is happening here --- it is extracting the column name from the error message.

3.  In your `ProductEditor` dialog add another `catch` to the existing try-catch block. This time it should catch the generic `Exception` and use a  `DaoExceptionMapper` object to convert the exception into a useful error message.  The  catch block looks like:

    ```java
    catch (Exception ex) {
       JOptionPane.showMessageDialog(this,
            new DaoExceptionMapper().messageFromException(ex),
            "Database Error", JOptionPane.ERROR_MESSAGE);
    }
    ```

     Some people consider it to be bad practice to catch generic errors like `Exception` or `RuntimeException`.  The reality is that the vast majority of the time there is absolutely nothing that you can do even if you catch a very specific exception --- the system has already broken --- we generally can't do much about correcting that via code.  The specific exceptions belong to either JDBI or H2, and we don't want to use those directly since that is an encapsulation leak.

    In our case the `DaoExceptionMapper` class is analysing the exception and trying to produce a useful message to the user --- the `DaoExceptionMapper` is looking for the specific exceptions to try to determine what went wrong.

4.  Some of the DAO operations are being performed in the constructors of the dialog classes, so we should also catch any errors that might be thrown if something goes wrong.  In the `MainMenu`, add a try-catch block around the code in the 'add new product' button handler.  The catch block is the same as used in the previous step.

5.  Do the same again for the 'view products' button handler.

6.  Run the system.  The `DaoExceptionMapper` is checking for 3 specific problems:

    *  Unique constraint violations.
    *  Other database constraint violations.
    *  Database connection problems.

    Can you cause all three of these messages to appear?

Note --- we could do a better job of this.  Instead of returning error messages that need to be generic enough to apply to whatever situation caused the error, we could instead return an error code (perhaps using an enumerated type).  The programmer can then use that error code to produce a much more useful error message that is specific to the situation that caused the error.  We don't expect you to do this since it requires a bit more work --- we just wanted to point out that this solution as presented can be improved.