diff --git a/README.md b/README.md index c09d8f1..54af334 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,60 @@ jelvis [![Build Status](https://travis-ci.org/lukaszbudnik/jelvis.svg?branch=master)](https://travis-ci.org/lukaszbudnik/jelvis) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.lukaszbudnik.jelvis/jelvis/badge.svg?style=flat)](https://maven-badges.herokuapp.com/maven-central/com.github.lukaszbudnik.jelvis/jelvis) ============================== -Elvis operator in Java 8 & Scala! Say no to NPE in chained calls! +jelvis is an elvis operator for Java 8 & Scala. jelvis eats `NullPointerException` in chained calls and returns null instead. + +For example in this chained call: + +``` +person.getAddress().getCountry() +``` + +you can get at least two NPEs: first when `person` is null and second when `getAddress()` returns null. + +jelvis takes care of NPEs for you. For the above call jelvis will return the value of `getCountry()` or null if either `person` or `getAddress()` is null. No NPE will be thrown. # Getting started ## Java 8 -1. Add jelvis to your project -2. Use the static `elvis` method like this: +All you need is just one static import: ``` import static com.github.lukaszbudnik.jelvis.Elvis.elvis; //... Person person = new Person(); +// old syntax requires 2 arguments +// the first argument to `elvis` method is the root object +// the second one is the lambda function to be evaluated String isoCode = elvis(person, p -> p.getAddress().getCountry().getISOCode()); -``` - -The first argument to `elvis` method is the root object and the second one is the function to be evaluated. -If either `person` is null or `getAddress()` or `getCountry()` returns null, the whole call returns null. +// new syntax takes just a lambda to be evaluated +// a little bit shorter than the old syntax. +String isoCode = elvis(() -> person.getAddress().getCountry().getISOCode()); +``` No `NullPointerException` is thrown. All other exceptions are preserved. -However, Java 8 lambdas have some problems with functions throwing checked exceptions. -If your methods throw checked exception you need to use `wrappedFunction` like this: +Java 8 lambdas have some problems with functions throwing checked exceptions (code simply does not compile). +To overcome this limitation jelvis is wrapping all checked exceptions into a runtime exception: `ElvisException`. + +Prior to jelvis 1.3 you had to explicitly wrap functions throwing exceptions using `Elvis.wrappedFunction()`. +This is now done automatically: ``` import static com.github.lukaszbudnik.jelvis.Elvis.elvis; -import static com.github.lukaszbudnik.jelvis.Elvis.wrappedFunction; //... Person person = new Person(); -String line2 = elvis(person, wrappedFunction(p -> p.getAddress().getLine2())); +// starting with jelvis 1.3 there is no need to use Elvis.wrappedFunction() +// old syntax +String line2 = elvis(person, p -> p.getAddress().getLine2()); +// new syntax +String line2 = elvis(() -> person.getAddress().getLine2()); ``` ## Scala -There is an implicit converter which converts Scala `Function1` into Java 8 `Function`. -All you have to do is import it in your code: +There is an implicit converter which converts Scala `Function1` into jelvis functions. +So in case of Scala you have to add two imports: ``` import com.github.lukaszbudnik.jelvis.Elvis._ @@ -47,11 +65,14 @@ And then just use the `elvis` method in your code: ``` val person = new Person +// old syntax val isoCode = elvis(person, (p: Person) => p.getAddress.getCountry.getISOCode) +// new syntax +val isoCode = elvis(() => person.getAddress.getCountry.getISOCode) ``` Exceptions? In Scala all exceptions are unchecked. You don't have to worry about them at all. -For example `p.getAddress.getLine2` doesn't have to be wrapped with `wrappedFunction`. +For example `p.getAddress.getLine2` doesn't have to be wrapped using `Elvis.wrappedFunction()`. Thus, when running from Scala, you will never see `ElvisException`. The call simply looks like this: diff --git a/build.gradle b/build.gradle index 148849e..47de2ba 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ targetCompatibility = JavaVersion.VERSION_1_8 group = 'com.github.lukaszbudnik.jelvis' archivesBaseName = 'jelvis' -version = '1.2' +version = '1.3.0' repositories { mavenCentral() @@ -119,8 +119,7 @@ uploadArchives { pom.project { name 'jelvis' packaging 'jar' - // optionally artifactId can be defined here - description 'Elvis operator in Java 8!' + description 'Elvis operator for Java 8 and Scala' url 'https://github.com/lukaszbudnik/jelvis' scm { diff --git a/src/main/java/com/github/lukaszbudnik/jelvis/Elvis.java b/src/main/java/com/github/lukaszbudnik/jelvis/Elvis.java index 622a6f9..4a63151 100644 --- a/src/main/java/com/github/lukaszbudnik/jelvis/Elvis.java +++ b/src/main/java/com/github/lukaszbudnik/jelvis/Elvis.java @@ -21,10 +21,22 @@ public final class Elvis { private Elvis() { } - public static R elvis(T t, Function f) { + public static R elvis(NoArgFunctionThrowingException f) { + try { + log.trace("About to apply no arg function " + f); + NoArgFunction wrappedFunction = wrappedFunction(f); + return wrappedFunction.apply(); + } catch (NullPointerException e) { + log.debug("NullPointerException caught - gracefully returning null instead", e); + return null; + } + } + + public static R elvis(T t, FunctionThrowingException f) { try { log.trace("About to apply object " + t + " on function " + f); - return f.apply(t); + Function wrappedFunction = wrappedFunction(f); + return wrappedFunction.apply(t); } catch (NullPointerException e) { log.debug("NullPointerException caught - gracefully returning null instead", e); return null; @@ -47,9 +59,20 @@ public static Function wrappedFunction(FunctionThrowingException { - R apply(T t) throws Exception; + public static NoArgFunction wrappedFunction(NoArgFunctionThrowingException f) { + log.trace("Wrapping function " + f); + return () -> { + try { + log.trace("Delegating non-arg function " + f); + return f.apply(); + } catch (RuntimeException e) { + log.debug("RuntimeException caught - re-throwing as is", e); + throw e; + } catch (Exception e) { + log.debug("Checked Exception caught - wrapping into ElvisException and re-throwing", e); + throw new ElvisException("Checked exception was thrown", e); + } + }; } } diff --git a/src/main/java/com/github/lukaszbudnik/jelvis/FunctionalInterfaces.java b/src/main/java/com/github/lukaszbudnik/jelvis/FunctionalInterfaces.java new file mode 100644 index 0000000..d0f7d75 --- /dev/null +++ b/src/main/java/com/github/lukaszbudnik/jelvis/FunctionalInterfaces.java @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2015 Łukasz Budnik + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ +package com.github.lukaszbudnik.jelvis; + +@FunctionalInterface +interface FunctionThrowingException { + R apply(T t) throws Exception; +} + +@FunctionalInterface +interface NoArgFunctionThrowingException { + R apply() throws Exception; +} + +@FunctionalInterface +interface NoArgFunction { + R apply(); +} diff --git a/src/main/scala/com/github/lukaszbudnik/jelvis/ElvisScalaToJavaConverters.scala b/src/main/scala/com/github/lukaszbudnik/jelvis/ElvisScalaToJavaConverters.scala index f1c2986..3658b02 100644 --- a/src/main/scala/com/github/lukaszbudnik/jelvis/ElvisScalaToJavaConverters.scala +++ b/src/main/scala/com/github/lukaszbudnik/jelvis/ElvisScalaToJavaConverters.scala @@ -9,14 +9,14 @@ */ package com.github.lukaszbudnik.jelvis -import java.util.function.{Function => JFunction} - object ElvisScalaToJavaConverters { - implicit def toJavaFunction[A, B](f: Function1[A, B]) = new JFunction[A, B] { - override def apply(a: A): B = f(a) + implicit def toJavaFunction[T, R](f: Function1[T, R]) = new FunctionThrowingException[T, R] { + override def apply(t: T): R = f(t) + } - def fromScala: Boolean = true + implicit def toJavaFunction[R](f: Function0[R]) = new NoArgFunctionThrowingException[R] { + override def apply(): R = f() } } diff --git a/src/test/java/com/github/lukaszbudnik/jelvis/ElvisNewSyntaxTest.java b/src/test/java/com/github/lukaszbudnik/jelvis/ElvisNewSyntaxTest.java new file mode 100644 index 0000000..6eeaaf0 --- /dev/null +++ b/src/test/java/com/github/lukaszbudnik/jelvis/ElvisNewSyntaxTest.java @@ -0,0 +1,95 @@ +/** + * Copyright (C) 2015 Łukasz Budnik + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ +package com.github.lukaszbudnik.jelvis; + +import com.github.lukaszbudnik.jelvis.Model.Address; +import com.github.lukaszbudnik.jelvis.Model.Person; +import org.junit.Assert; +import org.junit.Test; + +import static com.github.lukaszbudnik.jelvis.Elvis.elvis; + +public class ElvisNewSyntaxTest { + + @Test + public void shouldReturnNotNullWhenAllIsGood() { + Person person = new Person(); + Address address = elvis(() -> person.getAddress()); + + Assert.assertNotNull(address); + } + + @Test + public void shouldReturnNotNullWhenAllIsGoodWrappedFunction() { + Person person = new Person(); + int iq = elvis(() -> person.getIQ()); + + Assert.assertEquals(100, iq); + } + + @Test + public void shouldReturnNullWhenChainedGetterReturnsNull() { + Person person = new Person(); + String isoCode = elvis(() -> person.getAddress().getCountry().getISOCode()); + + Assert.assertNull(isoCode); + } + + @Test + public void shouldReturnNullWhenRootObjectIsNull() { + Person person = null; + String isoCode = elvis(() -> person.getAddress().getCountry().getISOCode()); + + Assert.assertNull(isoCode); + } + + @Test + public void shouldThrowOriginalRuntimeException() { + Person person = new Person(); + + try { + elvis(() -> person.getAddress().getLine1()); + Assert.fail(); + } catch (RuntimeException e) { + Assert.assertEquals("getLine1 threw runtime exception", e.getMessage()); + } catch (Throwable t) { + Assert.fail(); + } + } + + @Test + public void shouldThrowElvisExceptionForCheckedExceptions() { + Person person = new Person(); + + try { + elvis(() -> person.getAddress().getLine2()); + Assert.fail("Exception was expected"); + } catch (ElvisException e) { + Assert.assertEquals("getLine2 threw checked exception", e.getCause().getMessage()); + } catch (Throwable t) { + Assert.fail("Throwable was not expected"); + } + } + + @Test + public void shouldThrowRuntimeExceptionCorrectlyEvenIfMethodDeclaresCheckedException() { + Person person = new Person(); + + try { + elvis(() -> person.getAddress().getGeoLocation()); + Assert.fail("Exception was expected"); + } catch (RuntimeException e) { + Assert.assertEquals("Geo location declares Exception but is throwing runtime exception", e.getMessage()); + } catch (Throwable t) { + Assert.fail("Throwable was not expected"); + } + } + +} diff --git a/src/test/java/com/github/lukaszbudnik/jelvis/ElvisTest.java b/src/test/java/com/github/lukaszbudnik/jelvis/ElvisTest.java index c82d197..a03b459 100644 --- a/src/test/java/com/github/lukaszbudnik/jelvis/ElvisTest.java +++ b/src/test/java/com/github/lukaszbudnik/jelvis/ElvisTest.java @@ -15,7 +15,6 @@ import org.junit.Test; import static com.github.lukaszbudnik.jelvis.Elvis.elvis; -import static com.github.lukaszbudnik.jelvis.Elvis.wrappedFunction; public class ElvisTest { @@ -30,7 +29,7 @@ public void shouldReturnNotNullWhenAllIsGood() { @Test public void shouldReturnNotNullWhenAllIsGoodWrappedFunction() { Person person = new Person(); - int iq = elvis(person, wrappedFunction(p -> p.getIQ())); + int iq = elvis(person, p -> p.getIQ()); Assert.assertEquals(100, iq); } @@ -70,7 +69,7 @@ public void shouldThrowElvisExceptionForCheckedExceptions() { Person person = new Person(); try { - elvis(person, wrappedFunction(p -> p.getAddress().getLine2())); + elvis(person, p -> p.getAddress().getLine2()); Assert.fail("Exception was expected"); } catch (ElvisException e) { Assert.assertEquals("getLine2 threw checked exception", e.getCause().getMessage()); @@ -84,7 +83,7 @@ public void shouldThrowRuntimeExceptionCorrectlyEvenIfMethodDeclaresCheckedExcep Person person = new Person(); try { - elvis(person, wrappedFunction(p -> p.getAddress().getGeoLocation())); + elvis(person, p -> p.getAddress().getGeoLocation()); Assert.fail("Exception was expected"); } catch (RuntimeException e) { Assert.assertEquals("Geo location declares Exception but is throwing runtime exception", e.getMessage()); @@ -94,4 +93,3 @@ public void shouldThrowRuntimeExceptionCorrectlyEvenIfMethodDeclaresCheckedExcep } } - diff --git a/src/test/scala/com/github/lukaszbudnik/jelvis/ElvisSpec.scala b/src/test/scala/com/github/lukaszbudnik/jelvis/ElvisSpec.scala index 7d20d48..ff5641b 100644 --- a/src/test/scala/com/github/lukaszbudnik/jelvis/ElvisSpec.scala +++ b/src/test/scala/com/github/lukaszbudnik/jelvis/ElvisSpec.scala @@ -14,6 +14,7 @@ import com.github.lukaszbudnik.jelvis.Elvis._ import com.github.lukaszbudnik.jelvis.ElvisScalaToJavaConverters._ import com.github.lukaszbudnik.jelvis.Model._ import org.junit.runner.RunWith +import org.specs2.matcher.{MatchResult, Expectable, Matcher} import org.specs2.mutable.Specification import org.specs2.runner.JUnitRunner @@ -29,6 +30,14 @@ class ElvisSpec extends Specification { iq must beEqualTo(100) } + "return expected values when all is good - using new syntax" in { + val person: Person = new Person + + val iq: Int = elvis(() => person.getIQ) + + iq must beEqualTo(100) + } + "return null when chained call throws NPE" in { val person = new Person val isoCode = elvis(person, (p: Person) => p.getAddress.getCountry.getISOCode) @@ -39,9 +48,10 @@ class ElvisSpec extends Specification { "throw ElvisException when chained call throws checked exception" in { val person = new Person - elvis(person, (p: Person) => p.getAddress.getLine2) must throwAn[Exception]("getLine2 threw checked exception") + elvis(person, (p: Person) => p.getAddress.getLine2) must throwAn[Exception].like { + case e: ElvisException => e.getCause.getMessage must equalTo("getLine2 threw checked exception") + } } - } }