Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better linkTo syntax #143

Merged
merged 7 commits into from
Sep 30, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,22 @@
package br.com.caelum.vraptor.view;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
Expand All @@ -35,6 +46,11 @@

import br.com.caelum.vraptor.controller.BeanClass;
import br.com.caelum.vraptor.http.route.Router;
import br.com.caelum.vraptor.proxy.MethodInvocation;
import br.com.caelum.vraptor.proxy.Proxifier;
import br.com.caelum.vraptor.proxy.ProxyCreationException;
import br.com.caelum.vraptor.proxy.SuperMethod;
import br.com.caelum.vraptor.util.StringUtils;

import com.google.common.collect.ForwardingMap;

Expand All @@ -53,14 +69,19 @@ public class LinkToHandler extends ForwardingMap<Class<?>, Object> {
private ServletContext context;
private Router router;

private Proxifier proxifier;

private ConcurrentMap<Class<?>, Class<?>> interfaces = new ConcurrentHashMap<>();

@Deprecated // CDI eyes only
public LinkToHandler() {
}

@Inject
public LinkToHandler(ServletContext context, Router router) {
public LinkToHandler(ServletContext context, Router router, Proxifier proxifier) {
this.context = context;
this.router = router;
this.proxifier = proxifier;
}

@PostConstruct
Expand All @@ -73,64 +94,76 @@ protected Map<Class<?>, Object> delegate() {
return Collections.emptyMap();
}

private Lock lock = new ReentrantLock();

@Override
public LinkMethod get(Object key) {
public Object get(Object key) {
BeanClass beanClass = (BeanClass) key;
return new LinkMethod(beanClass.getType());
}

class LinkMethod extends ForwardingMap<String, Linker> {

private final Class<?> controller;
public LinkMethod(Class<?> controller) {
this.controller = controller;
}
@Override
protected Map<String, Linker> delegate() {
return Collections.emptyMap();
final Class<?> controller = beanClass.getType();
Class<?> linkToInterface = interfaces.get(controller);
if (linkToInterface == null) {
lock.lock();
try {
linkToInterface = interfaces.get(controller);
if (linkToInterface == null) {
String interfaceName = controller.getName() + "$linkTo";
linkToInterface = createLinkToInterface(controller, interfaceName);
interfaces.put(controller, linkToInterface);
}
} finally {
lock.unlock();
}
}
return proxifier.proxify(linkToInterface, new MethodInvocation<Object>() {
@Override
public Object intercept(Object proxy, Method method, Object[] args, SuperMethod superMethod) {
String methodName = StringUtils.decapitalize(method.getName().replaceFirst("^get", ""));
List<Object> params = args.length == 0 ? Collections.emptyList() : Arrays.asList((Object[]) args[0]);
return new Linker(controller, methodName, params).getLink();
}
});
}

@Override
public Linker get(Object key) {
return new Linker(controller, key.toString());
private Class<?> createLinkToInterface(final Class<?> controller, String interfaceName) {
ClassPool pool = ClassPool.getDefault();
CtClass inter = pool.makeInterface(interfaceName);
try {
for(String name : getMethodNames(controller)) {
CtMethod method = CtNewMethod.make(String.format("abstract String %s(Object[] args);", name), inter);
method.setModifiers(method.getModifiers() | 128 /* Modifier.VARARGS */);
inter.addMethod(method);
CtMethod getter = CtNewMethod.make(String.format("abstract String get%s();", StringUtils.capitalize(name)), inter);
inter.addMethod(getter);
}
return inter.toClass();
} catch (CannotCompileException e) {
throw new ProxyCreationException(e);
}
}

@Override
public String toString() {
throw new IllegalArgumentException("uncomplete linkTo[" + controller.getSimpleName() + "]. You must specify the method.");
private Set<String> getMethodNames(Class<?> controller) {
Set<String> names = new HashSet<>();
for (Method method : new Mirror().on(controller).reflectAll().methods()) {
if (!method.getDeclaringClass().equals(Object.class)) {
names.add(method.getName());
}
}
return names;
}

class Linker extends ForwardingMap<Object, Linker> {
class Linker {

private final List<Object> args;
private final String methodName;
private final Class<?> controller;

public Linker(Class<?> controller, String methodName) {
this(controller, methodName, new ArrayList<>());
}

public Linker(Class<?> controller, String methodName, List<Object> args) {
this.controller = controller;
this.methodName = methodName;
this.args = args;
}

@Override
public Linker get(Object key) {
List<Object> newArgs = new ArrayList<>(args);
newArgs.add(key);
return new Linker(controller, methodName, newArgs);
}

@Override
protected Map<Object, Linker> delegate() {
return Collections.emptyMap();
}

@Override
public String toString() {
public String getLink() {
Method method = null;

if (getMethodsAmountWithSameName() > 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.when;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import javax.servlet.ServletContext;

import net.vidageek.mirror.dsl.Mirror;
import net.vidageek.mirror.exception.MirrorException;

import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import br.com.caelum.vraptor.controller.DefaultBeanClass;
import br.com.caelum.vraptor.http.route.Router;
import br.com.caelum.vraptor.proxy.JavassistProxifier;

public class LinkToHandlerTest {
private @Mock ServletContext context;
Expand All @@ -29,101 +33,124 @@ public class LinkToHandlerTest {
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
this.handler = new LinkToHandler(context, router);
this.handler = new LinkToHandler(context, router, new JavassistProxifier());
when(context.getContextPath()).thenReturn("/path");

this.method2params = new Mirror().on(TestController.class).reflect().method("method").withArgs(String.class, int.class);
this.method1param = new Mirror().on(TestController.class).reflect().method("method").withArgs(String.class);
this.anotherMethod = new Mirror().on(TestController.class).reflect().method("anotherMethod").withArgs(String.class, int.class);
}

@Ignore("Does it worth?")
@Test(expected = IllegalArgumentException.class)
public void shouldThrowExceptionWhenInvocationIsIncomplete() {
//${linkTo[TestController]}
handler.get(new DefaultBeanClass(TestController.class)).toString();
}

@Ignore("The method won't exist")
@Test(expected = IllegalArgumentException.class)
public void shouldThrowExceptionWhenInvokingInexistantMethod() {
public void shouldThrowExceptionWhenInvokingInexistantMethod() throws Throwable {
//${linkTo[TestController].nonExists}
handler.get(new DefaultBeanClass(TestController.class)).get("nonExists").toString();
invoke(handler.get(new DefaultBeanClass(TestController.class)), "nonExists");
}

@Test(expected = IllegalArgumentException.class)
public void shouldThrowExceptionWhenMethodIsAmbiguous() {
//${linkTo[TestController].method}
handler.get(new DefaultBeanClass(TestController.class)).get("method").toString();
public void shouldThrowExceptionWhenMethodIsAmbiguous() throws Throwable {
//${linkTo[TestController].method()}
invoke(handler.get(new DefaultBeanClass(TestController.class)), "method");
}

@Test(expected = IllegalArgumentException.class)
public void shouldThrowExceptionWhenUsingParametersOfWrongTypes() {
//${linkTo[TestController].method[123]}
handler.get(new DefaultBeanClass(TestController.class)).get("method").get(123).toString();
public void shouldThrowExceptionWhenUsingParametersOfWrongTypes() throws Throwable {
//${linkTo[TestController].method(123)}
invoke(handler.get(new DefaultBeanClass(TestController.class)), "method", 123);
}


@Test
public void shouldReturnWantedUrlWithoutArgs() {
public void shouldReturnWantedUrlWithoutArgs() throws Throwable {
when(router.urlFor(TestController.class, anotherMethod, new Object[2])).thenReturn("/expectedURL");

//${linkTo[TestController].anotherMethod()}
String uri = invoke(handler.get(new DefaultBeanClass(TestController.class)), "anotherMethod");
assertThat(uri, is("/path/expectedURL"));
}

@Test
public void shouldReturnWantedUrlWithoutArgsUsingPropertyAccess() throws Throwable {
when(router.urlFor(TestController.class, anotherMethod, new Object[2])).thenReturn("/expectedURL");

//${linkTo[TestController].anotherMethod}
String uri = handler.get(new DefaultBeanClass(TestController.class)).get("anotherMethod").toString();
String uri = invoke(handler.get(new DefaultBeanClass(TestController.class)), "getAnotherMethod");
assertThat(uri, is("/path/expectedURL"));
}

@Test
public void shouldReturnWantedUrlWithParamArgs() {
public void shouldReturnWantedUrlWithParamArgs() throws Throwable {
String a = "test";
int b = 3;
when(router.urlFor(TestController.class, method2params, new Object[]{a, b})).thenReturn("/expectedURL");
//${linkTo[TestController].method['test'][3]}
String uri = handler.get(new DefaultBeanClass(TestController.class)).get("method").get(a).get(b).toString();
//${linkTo[TestController].method('test', 3)}
String uri = invoke(handler.get(new DefaultBeanClass(TestController.class)), "method", a, b);
assertThat(uri, is("/path/expectedURL"));
}

@Test
public void shouldReturnWantedUrlWithPartialParamArgs() {
public void shouldReturnWantedUrlWithPartialParamArgs() throws Throwable {
String a = "test";
when(router.urlFor(TestController.class, anotherMethod, new Object[]{a, null})).thenReturn("/expectedUrl");
//${linkTo[TestController].anotherMethod['test']}
String uri = handler.get(new DefaultBeanClass(TestController.class)).get("anotherMethod").get(a).toString();
//${linkTo[TestController].anotherMethod('test')}
String uri = invoke(handler.get(new DefaultBeanClass(TestController.class)), "anotherMethod", a);
assertThat(uri, is("/path/expectedUrl"));
}

@Test
public void shouldReturnWantedUrlForOverrideMethodWithParamArgs() throws NoSuchMethodException, SecurityException {
public void shouldReturnWantedUrlForOverrideMethodWithParamArgs() throws Throwable {
String a = "test";
when(router.urlFor(SubGenericController.class, SubGenericController.class.getDeclaredMethod("method", new Class[]{String.class}), new Object[]{a})).thenReturn("/expectedURL");
//${linkTo[TestSubGenericController].method['test']}]
String uri = handler.get(new DefaultBeanClass(SubGenericController.class)).get("method").get(a).toString();
//${linkTo[TestSubGenericController].method('test')}]
String uri = invoke(handler.get(new DefaultBeanClass(SubGenericController.class)), "method", a);
assertThat(uri, is("/path/expectedURL"));
}

@Test
public void shouldReturnWantedUrlForOverrideMethodWithParialParamArgs() throws SecurityException, NoSuchMethodException {
public void shouldReturnWantedUrlForOverrideMethodWithParialParamArgs() throws Throwable {
String a = "test";
when(router.urlFor(SubGenericController.class, SubGenericController.class.getDeclaredMethod("anotherMethod", new Class[]{String.class, String.class}), new Object[]{a, null})).thenReturn("/expectedURL");
//${linkTo[TestSubGenericController].anotherMethod['test']}]
String uri = handler.get(new DefaultBeanClass(SubGenericController.class)).get("anotherMethod").get(a).toString();
//${linkTo[TestSubGenericController].anotherMethod('test')}]
String uri = invoke(handler.get(new DefaultBeanClass(SubGenericController.class)), "anotherMethod", a);
assertThat(uri, is("/path/expectedURL"));
}

@Test
public void shouldUseExactlyMatchedMethodIfTheMethodIsOverloaded() {
public void shouldUseExactlyMatchedMethodIfTheMethodIsOverloaded() throws Throwable {
String a = "test";
when(router.urlFor(TestController.class, method1param, a)).thenReturn("/expectedUrl");
//${linkTo[TestController].method['test']}
String uri = handler.get(new DefaultBeanClass(TestController.class)).get("method").get(a).toString();
//${linkTo[TestController].method('test')}
String uri = invoke(handler.get(new DefaultBeanClass(TestController.class)), "method", a);
assertThat(uri, is("/path/expectedUrl"));
}

@Test(expected = IllegalArgumentException.class)
public void shouldThrowExceptionWhenPassingMoreArgsThanMethodSupports() {
public void shouldThrowExceptionWhenPassingMoreArgsThanMethodSupports() throws Throwable {
String a = "test";
int b = 3;
String c = "anotherTest";
//${linkTo[TestController].anotherMethod['test'][3]['anotherTest']}
handler.get(new DefaultBeanClass(TestController.class)).get("anotherMethod").get(a).get(b).get(c).toString();
//${linkTo[TestController].anotherMethod('test', 3, 'anotherTest')}
invoke(handler.get(new DefaultBeanClass(TestController.class)), "anotherMethod", a, b, c);
}

private String invoke(Object obj, String methodName, Object...args) throws Throwable {
try {
Method method = new Mirror().on(obj.getClass()).reflect().method(methodName).withAnyArgs();
if (methodName.startsWith("get")) {
return method.invoke(obj).toString();
}
return method.invoke(obj, (Object) args).toString();
} catch (MirrorException | InvocationTargetException e) {
throw e.getCause() == null? e : e.getCause();
}
}

static class TestController {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<tbody>
<c:forEach var="music" items="${musics}">
<tr>
<td><a href="${linkTo[MusicController].show[music]}">${music.title}</a></td>
<td><a href="${linkTo[MusicController].show(music)}">${music.title}</a></td>
<td>${music.description}</td>
<td><fmt:message key="${music.type}"/></td>
<td>
Expand All @@ -25,7 +25,7 @@
</c:forEach>
</td>
<td width="1px">
<form action="${linkTo[MusicOwnerController].addToMyList[userInfo.user][music]}" method="post">
<form action="${linkTo[MusicOwnerController].addToMyList(userInfo.user, music)}" method="post">
<input type="hidden" name="_method" value="PUT"/>
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-plus"></span>
Expand All @@ -34,7 +34,7 @@
</form>
</td>
<td width="1px">
<a href="${linkTo[MusicController].download[music]}" class="btn btn-primary" download>
<a href="${linkTo[MusicController].download(music)}" class="btn btn-primary" download>
<i class="glyphicon glyphicon-download-alt"></i>
download
</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
</strong>

<c:forEach items="${music.musicOwners}" var="mo" varStatus="s">
<a href="${linkTo[UsersController].show[mo.owner]}">${mo.owner.name}</a>
<a href="${linkTo[UsersController].show(mo.owner)}">${mo.owner.name}</a>
${s.last ? '.' : ', ' }
</c:forEach>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@
<tbody>
<c:forEach var="music" items="${userInfo.user.musics}" varStatus="s">
<tr>
<td><a href="${linkTo[MusicController].show[music]}">${music.title}</a></td>
<td><a href="${linkTo[MusicController].show(music)}">${music.title}</a></td>
<td>${music.description}</td>
<td><fmt:message key="${music.type}"/></td>
<td><a href="${linkTo[MusicController].download[music]}" download>download</a></td>
<td><a href="${linkTo[MusicController].download(music)}" download>download</a></td>
</tr>
</c:forEach>
</tbody>
Expand Down
Loading