/* * Copyright 2013 eXo Platform SAS * * 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 juzu.impl.plugin.controller; import juzu.request.Phase; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Controller method resolution algorithm. * <p/> * Resolves a controller method for a specified set of parameter names. The algorithm attempts to resolve a single value * with the following algorithm: * <p/> * <ul> <li>Filter the list of controllers, this is specific to the resolve method.</li> <li>When no method is retained, * the null value is returned.</li> <li>When several methods are retained the resulting list is sorted according * <i>resolution order</i>. If a first value is greater than all the others, this result is returned, otherwise a {@link * AmbiguousResolutionException} is thrown.</li> </ul> * <p/> * The <i>resolution order</i> uses three criteria for comparing two methods in the context of the specified parameter * names. * <p/> * <ol> <li>The greater number of matched specified parameters.</li> <li>The lesser number of unmatched method * arguments.</li> <li>The lesser number of unmatched method parameters.</li> <li>The default controller class.</li> * </ol> * * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a> */ public abstract class ControllerResolver<M> { // todo : take in account multi valued parameters // todo : what happens with type conversion, somehow we should forbid m(String a) and m(int a) public abstract M[] getHandlers(); public abstract String getId(M method); public abstract Phase getPhase(M method); public abstract String getName(M method); public abstract boolean isDefault(M method); public final boolean isIndex(M method) { return isDefault(method) && "index".equals(getName(method)); } public abstract Collection<String> getParameterNames(M method); private class Match implements Comparable<Match> { /** . */ final M method; /** . */ final int score1; /** . */ final int score2; /** . */ final int score3; /** . */ final int score4; Match(Set<String> parameterNames, M method) { this.method = method; // The number of matched parameters HashSet<String> a = new HashSet<String>(parameterNames); a.retainAll(getParameterNames(method)); this.score1 = a.size(); // The number of unmatched arguments a = new HashSet<String>(getParameterNames(method)); a.removeAll(parameterNames); this.score2 = a.size(); // The number of unmatched parameters a = new HashSet<String>(parameterNames); a.removeAll(getParameterNames(method)); this.score3 = a.size(); // The default method this.score4 = isDefault(method) ? 0 : 1; } public int compareTo(Match o) { int delta = o.score1 - score1; if (delta == 0) { delta = score2 - o.score2; if (delta == 0) { delta = score3 - o.score3; if (delta == 0) { delta = score4 - o.score4; } } } return delta; } @Override public String toString() { return "Match[score1=" + score1 + ",score2=" + score2 + ",score3=" + score3 + ",score4=" + score4 + ",method=" + method + "]"; } } /** * A method matches the filter when it has the render phase and the name <code>index</code>. * * @param phase the phare to match * @param parameterNames the parameter names * @return the resolved controller method * @throws NullPointerException if the parameter names set is nul * @throws AmbiguousResolutionException * if more than a single result is found */ public final M resolve(Phase phase, Set<String> parameterNames) throws NullPointerException, AmbiguousResolutionException { if (parameterNames == null) { throw new NullPointerException("No null parameter names accepted"); } // List<Match> matches = new ArrayList<Match>(); for (M method : getHandlers()) { if (getPhase(method) == phase) { if (phase == Phase.VIEW) { if (getName(method).equals("index")) { matches.add(new Match(parameterNames, method)); } } else { matches.add(new Match(parameterNames, method)); } } } // return select(matches); } /** * A method matches the filter when it matches the phase and the method id. * * @param phase the phrase * @param methodId the method id * @param parameterNames the parameter names * @return the resolved controller method * @throws NullPointerException if any parameter is nul * @throws AmbiguousResolutionException * if more than a single result is found */ public final M resolveMethod(Phase phase, String methodId, final Set<String> parameterNames) throws NullPointerException, AmbiguousResolutionException { if (parameterNames == null) { throw new NullPointerException("No null parameter names accepted"); } if (phase == null) { throw new NullPointerException("Phase parameter cannot be null"); } // List<Match> matches = new ArrayList<Match>(); for (M method : getHandlers()) { if (getPhase(method) == phase && (methodId == null || methodId.equals(getId(method)))) { matches.add(new Match(parameterNames, method)); } } // return select(matches); } /** * A method matches the filter when it matches the phase and the method id. * * @param phase the phrase * @param methodId the method id * @param parameterNames the parameter names * @return the resolved controller method * @throws NullPointerException if any parameter is nul * @throws AmbiguousResolutionException * if more than a single result is found */ public final List<M> resolveMethods(Phase phase, String methodId, final Set<String> parameterNames) throws NullPointerException, AmbiguousResolutionException { if (parameterNames == null) { throw new NullPointerException("No null parameter names accepted"); } if (phase == null) { throw new NullPointerException("Phase parameter cannot be null"); } // List<Match> matches = new ArrayList<Match>(); for (M method : getHandlers()) { if (getPhase(method) == phase && (methodId == null || methodId.equals(getId(method)))) { matches.add(new Match(parameterNames, method)); } } // Collections.sort(matches); ArrayList<M> methods = new ArrayList<M>(matches.size()); for (Match match : matches) { methods.add(match.method); } // return methods; } /** * A method matches the filter when it matches the type name, the method name and contains all the parameters. * * @param typeName the optional type name * @param methodName the method name * @param parameterNames the parameter names * @return the resolved controller method * @throws NullPointerException if the methodName or parameterNames argument is null * @throws AmbiguousResolutionException * if more than a single result is found */ public M resolve(String typeName, String methodName, Set<String> parameterNames) throws NullPointerException, AmbiguousResolutionException { if (parameterNames == null) { throw new NullPointerException("No null parameter names accepted"); } if (methodName == null) { throw new NullPointerException("Phase parameter cannot be null"); } // List<Match> matches = new ArrayList<Match>(); for (M method : getHandlers()) { if (getParameterNames(method).containsAll(parameterNames)) { if (typeName == null) { if (getName(method).equals(methodName)) { matches.add(new Match(parameterNames, method)); } } else { String id = typeName + "." + methodName; if (getId(method).equals(id)) { matches.add(new Match(parameterNames, method)); } } } } // return select(matches); } private M select(List<Match> matches) throws AmbiguousResolutionException { M found = null; if (matches.size() > 0) { Collections.sort(matches); Match first = matches.get(0); if (matches.size() > 1) { Match second = matches.get(1); if (first.compareTo(second) == 0) { throw new AmbiguousResolutionException("Two methods satisfies the index criteria: " + first.method + " and " + second.method); } } found = first.method; } return found; } }