diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f58fd5e --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Java # +*.class + +# Package Files # +*.jar +*.war +*.ear + +# IDEA # +*.iml +.idea +*~ + +# eclipse specific git ignore +.project +.metadata +.classpath +.settings/ + +# Maven files # +data/ +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties + +# Misc # +*.log +*.bat + diff --git a/cdi/pom.xml b/cdi/pom.xml new file mode 100644 index 0000000..3b8acc8 --- /dev/null +++ b/cdi/pom.xml @@ -0,0 +1,56 @@ + + + + 4.0.0 + + + com.github.mbenson.therian + therian-parent + 0.6-SNAPSHOT + + + therian-cdi + therian CDI extension + jar + + + + org.apache.openejb + javaee-api + 6.0-6 + provided + + + com.github.mbenson.therian + therian + ${project.version} + + + + junit + junit + + + org.apache.openejb + openejb-core + 4.7.2 + test + + + diff --git a/cdi/src/main/java/therian/cdi/Mapper.java b/cdi/src/main/java/therian/cdi/Mapper.java new file mode 100644 index 0000000..f63b4ab --- /dev/null +++ b/cdi/src/main/java/therian/cdi/Mapper.java @@ -0,0 +1,26 @@ +/* + * Copyright the original author or authors. + * + * 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 therian.cdi; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Mapper { +} diff --git a/cdi/src/main/java/therian/cdi/internal/MapperBean.java b/cdi/src/main/java/therian/cdi/internal/MapperBean.java new file mode 100644 index 0000000..cacd2de --- /dev/null +++ b/cdi/src/main/java/therian/cdi/internal/MapperBean.java @@ -0,0 +1,107 @@ +/* + * Copyright the original author or authors. + * + * 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 therian.cdi.internal; + +import therian.cdi.internal.literal.AnyLiteral; +import therian.cdi.internal.literal.DefaultLiteral; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.spi.AnnotatedType; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.InjectionPoint; +import java.lang.annotation.Annotation; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Set; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptySet; + +public class MapperBean implements Bean { + private final Set types; + private final Set qualifiers; + private final Class clazz; + private final Class[] proxyTypes; + private final MapperHandler handler; + + public MapperBean(final AnnotatedType at) { + clazz = at.getJavaClass(); + types = new HashSet<>(asList(clazz, Object.class)); + qualifiers = new HashSet<>(asList(DefaultLiteral.INSTANCE, AnyLiteral.INSTANCE)); + proxyTypes = new Class[] { clazz }; + handler = new MapperHandler(at); + } + + @Override + public Set getTypes() { + return types; + } + + @Override + public Set getQualifiers() { + return qualifiers; + } + + @Override + public Class getScope() { + return ApplicationScoped.class; + } + + @Override + public String getName() { + return null; + } + + @Override + public boolean isNullable() { + return false; + } + + @Override + public Set getInjectionPoints() { + return emptySet(); + } + + @Override + public Class getBeanClass() { + return clazz; + } + + @Override + public Set> getStereotypes() { + return emptySet(); + } + + @Override + public boolean isAlternative() { + return false; + } + + @Override + public T create(final CreationalContext context) { + final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + return (T) Proxy.newProxyInstance( + contextClassLoader == null ? ClassLoader.getSystemClassLoader() : contextClassLoader, + proxyTypes, handler); + } + + @Override + public void destroy(final T instance, final CreationalContext context) { + // no-op + } +} diff --git a/cdi/src/main/java/therian/cdi/internal/MapperHandler.java b/cdi/src/main/java/therian/cdi/internal/MapperHandler.java new file mode 100644 index 0000000..7625ee5 --- /dev/null +++ b/cdi/src/main/java/therian/cdi/internal/MapperHandler.java @@ -0,0 +1,110 @@ +/* + * Copyright the original author or authors. + * + * 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 therian.cdi.internal; + +import static java.util.Optional.of; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.function.Function; + +import javax.enterprise.inject.spi.AnnotatedMethod; +import javax.enterprise.inject.spi.AnnotatedType; + +import org.apache.commons.lang3.reflect.TypeUtils; +import org.apache.commons.lang3.reflect.Typed; + +import therian.Therian; +import therian.TherianModule; +import therian.operation.Convert; +import therian.operator.copy.PropertyCopier; +import therian.util.Positions; + +public class MapperHandler implements InvocationHandler { + private final Map> mapping; + private final String toString; + + public MapperHandler(final AnnotatedType type) { + // just for error handling + of(type.getMethods().stream() + .filter(m -> m.isAnnotationPresent(PropertyCopier.Mapping.class) + && (m.getParameters().size() != 1 || m.getJavaMember().getReturnType() == void.class)) + .collect(toList())).filter(l -> !l.isEmpty()).ifPresent(l -> { + throw new IllegalArgumentException("@Mapping only supports one parameter and not void signatures"); + }); + + // TODO: use a single Therian instance if there are not redundant conversions specified by interface methods + + this.mapping = type.getMethods().stream().filter(m -> m.isAnnotationPresent(PropertyCopier.Mapping.class)) + .collect(toMap(AnnotatedMethod::getJavaMember, am -> { + final Method member = am.getJavaMember(); + final Typed source = TypeUtils.wrap(member.getGenericParameterTypes()[0]); + final Typed target = TypeUtils.wrap(member.getGenericReturnType()); + + final Therian therian = + Therian.standard() + .withAdditionalModules(TherianModule.expandingDependencies(TherianModule.create() + .withOperators(PropertyCopier.getInstance(source, target, + am.getAnnotation(PropertyCopier.Mapping.class), + am.getAnnotation(PropertyCopier.Matching.class))))); + + @SuppressWarnings({ "unchecked", "rawtypes" }) + final Meta result = new Meta(therian, + sourceInstance -> Convert.to(target, Positions. readOnly(source, sourceInstance))); + return result; + })); + + this.toString = getClass().getSimpleName() + "[" + type.getJavaClass().getName() + "]"; + } + + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { + if (Object.class.equals(method.getDeclaringClass())) { + try { + return method.invoke(this, args); + } catch (final InvocationTargetException ite) { + throw ite.getCause(); + } + } + + @SuppressWarnings("unchecked") + final Meta meta = (Meta) mapping.get(method); + return meta.convert(args[0]); + } + + @Override + public String toString() { + return toString; + } + + private static final class Meta { + private final Therian therian; + private final Function> convert; + + public Meta(final Therian therian, final Function> convert) { + this.therian = therian; + this.convert = convert; + } + + public B convert(final A in) { + return therian.context().eval(convert.apply(in)); + } + } +} diff --git a/cdi/src/main/java/therian/cdi/internal/TherianExtension.java b/cdi/src/main/java/therian/cdi/internal/TherianExtension.java new file mode 100644 index 0000000..b51ad96 --- /dev/null +++ b/cdi/src/main/java/therian/cdi/internal/TherianExtension.java @@ -0,0 +1,42 @@ +/* + * Copyright the original author or authors. + * + * 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 therian.cdi.internal; + +import therian.cdi.Mapper; + +import javax.enterprise.event.Observes; +import javax.enterprise.inject.spi.AfterBeanDiscovery; +import javax.enterprise.inject.spi.AnnotatedType; +import javax.enterprise.inject.spi.Extension; +import javax.enterprise.inject.spi.ProcessAnnotatedType; +import java.util.ArrayList; +import java.util.Collection; + +public class TherianExtension implements Extension { + private final Collection> detectedMappers = new ArrayList<>(); + + void captureMapper(@Observes final ProcessAnnotatedType potentialMapper) { + final AnnotatedType annotatedType = potentialMapper.getAnnotatedType(); + if (annotatedType.isAnnotationPresent(Mapper.class)) { + detectedMappers.add(annotatedType); + } + } + + void addMapperBeans(@Observes final AfterBeanDiscovery abd) { + detectedMappers.stream().forEach(at -> abd.addBean(new MapperBean(at))); + detectedMappers.clear(); + } +} diff --git a/cdi/src/main/java/therian/cdi/internal/literal/AnyLiteral.java b/cdi/src/main/java/therian/cdi/internal/literal/AnyLiteral.java new file mode 100644 index 0000000..3f7b5c3 --- /dev/null +++ b/cdi/src/main/java/therian/cdi/internal/literal/AnyLiteral.java @@ -0,0 +1,30 @@ +/* + * Copyright the original author or authors. + * + * 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 therian.cdi.internal.literal; + +import javax.enterprise.inject.Any; + +public class AnyLiteral extends EmptyAnnotationLiteral implements Any { + public static final AnyLiteral INSTANCE = new AnyLiteral(); + + private static final String TOSTRING = "@javax.enterprise.inject.Any()"; + private static final long serialVersionUID = 6788272256977634238L; + + @Override + public String toString() { + return TOSTRING; + } +} diff --git a/cdi/src/main/java/therian/cdi/internal/literal/DefaultLiteral.java b/cdi/src/main/java/therian/cdi/internal/literal/DefaultLiteral.java new file mode 100644 index 0000000..48e0356 --- /dev/null +++ b/cdi/src/main/java/therian/cdi/internal/literal/DefaultLiteral.java @@ -0,0 +1,30 @@ +/* + * Copyright the original author or authors. + * + * 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 therian.cdi.internal.literal; + +import javax.enterprise.inject.Default; + +public class DefaultLiteral extends EmptyAnnotationLiteral implements Default { + public static final DefaultLiteral INSTANCE = new DefaultLiteral(); + + private static final String TOSTRING = "@javax.enterprise.inject.Default()"; + private static final long serialVersionUID = 6788272256977634238L; + + @Override + public String toString() { + return TOSTRING; + } +} diff --git a/cdi/src/main/java/therian/cdi/internal/literal/EmptyAnnotationLiteral.java b/cdi/src/main/java/therian/cdi/internal/literal/EmptyAnnotationLiteral.java new file mode 100644 index 0000000..d281db8 --- /dev/null +++ b/cdi/src/main/java/therian/cdi/internal/literal/EmptyAnnotationLiteral.java @@ -0,0 +1,70 @@ +/* + * Copyright the original author or authors. + * + * 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 therian.cdi.internal.literal; + +import javax.enterprise.util.AnnotationLiteral; +import java.lang.annotation.Annotation; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +public abstract class EmptyAnnotationLiteral extends AnnotationLiteral { + private Class annotationType; + + protected EmptyAnnotationLiteral() { + // no-op + } + + @Override + public Class annotationType() { + if (annotationType == null) { + annotationType = getAnnotationType(getClass()); + } + return annotationType; + } + + @Override + public int hashCode() { + return 0; + } + + @Override + public boolean equals(final Object other) { + return Annotation.class.isInstance(other) && + Annotation.class.cast(other).annotationType().equals(annotationType()); + } + + private Class getAnnotationType(final Class definedClazz) { + final Type superClazz = definedClazz.getGenericSuperclass(); + if (superClazz.equals(Object.class)) { + throw new IllegalArgumentException("Super class must be parametrized type!"); + } else if (superClazz instanceof ParameterizedType) { + final ParameterizedType paramType = (ParameterizedType) superClazz; + final Type[] actualArgs = paramType.getActualTypeArguments(); + + if (actualArgs.length == 1) { + final Type type = actualArgs[0]; + if (type instanceof Class) { + return (Class) type; + } else { + throw new IllegalArgumentException("Not class type!"); + } + } else { + throw new IllegalArgumentException("More than one parametric type!"); + } + } + return getAnnotationType((Class) superClazz); + } +} diff --git a/cdi/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/cdi/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension new file mode 100644 index 0000000..174a1c0 --- /dev/null +++ b/cdi/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension @@ -0,0 +1 @@ +therian.cdi.internal.TherianExtension \ No newline at end of file diff --git a/cdi/src/test/java/therian/cdi/internal/TherianExtensionTest.java b/cdi/src/test/java/therian/cdi/internal/TherianExtensionTest.java new file mode 100644 index 0000000..531ed87 --- /dev/null +++ b/cdi/src/test/java/therian/cdi/internal/TherianExtensionTest.java @@ -0,0 +1,102 @@ +/* + * Copyright the original author or authors. + * + * 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 therian.cdi.internal; + +import org.apache.openejb.jee.WebApp; +import org.apache.openejb.junit.ApplicationComposer; +import org.apache.openejb.testing.Classes; +import org.apache.openejb.testing.Module; +import org.junit.Test; +import org.junit.runner.RunWith; +import therian.cdi.Mapper; +import therian.operator.copy.PropertyCopier.Mapping.Value; + +import javax.inject.Inject; + +import static org.junit.Assert.assertEquals; + +@RunWith(ApplicationComposer.class) +public class TherianExtensionTest { + @Module + @Classes(cdi = true, value = FromToMapper.class) + public WebApp app() { + return new WebApp(); + } + + @Inject + private FromToMapper mapper; + + @Test + public void copy() { + final From from = new From(); + from.setInteger(1234); + from.setString("degfebfek"); + + final To to = mapper.to(from); + + assertEquals(1234, to.getCount()); + assertEquals("degfebfek", to.getDescription()); + } + + @Mapper + public interface FromToMapper { + @Value(from = "integer", to = "count") + @Value(from = "string", to = "description") + To to(From from); + } + + public static class From { + private int integer; + private String string; + + public int getInteger() { + return integer; + } + + public void setInteger(final int integer) { + this.integer = integer; + } + + public java.lang.String getString() { + return string; + } + + public void setString(final String string) { + this.string = string; + } + } + + public static class To { + private int count; + private String description; + + public int getCount() { + return count; + } + + public void setCount(final int count) { + this.count = count; + } + + public java.lang.String getDescription() { + return description; + } + + public void setDescription(final String description) { + this.description = description; + } + } +} diff --git a/core/src/main/java/therian/Therian.java b/core/src/main/java/therian/Therian.java index 3017bc9..e765e08 100755 --- a/core/src/main/java/therian/Therian.java +++ b/core/src/main/java/therian/Therian.java @@ -28,6 +28,7 @@ import javax.el.ELContextListener; import javax.el.ELResolver; +import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.Validate; import uelbox.ELContextWrapper; @@ -94,6 +95,18 @@ private Therian(TherianModule... modules) { operatorManager = new OperatorManager(operators); } + + /** + * Obtain a {@link Therian} instance with the modules configured in addition to those of this instance. + * @param modules + * @return Therian + */ + public Therian withAdditionalModules(TherianModule... modules) { + if (ArrayUtils.isEmpty(modules)) { + return this; + } + return new Therian(ArrayUtils.addAll(this.modules, modules)); + } public TherianContext context() { return contextFor(new SimpleELContext()); diff --git a/core/src/main/java/therian/operator/copy/PropertyCopier.java b/core/src/main/java/therian/operator/copy/PropertyCopier.java index a0f598a..fa06c6b 100755 --- a/core/src/main/java/therian/operator/copy/PropertyCopier.java +++ b/core/src/main/java/therian/operator/copy/PropertyCopier.java @@ -17,6 +17,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -78,6 +79,7 @@ public abstract class PropertyCopier extends Copier build-processor core + cdi