Статьи

Гобелен Магия: легкий выбор

Гобелен в выберите компонент является очень гибким и эта гибкость приходит за небольшую плату. Нужно передать SelectModel и ValueEncoder в качестве параметров компоненту Select. Это кажется слишком большой работой для простых случаев использования. С помощью преобразований классов мы всегда можем найти ярлыки. В этой статье я расскажу о некоторых полезных методах, упомянутых в вики, чтобы упростить использование компонента Select.

использование

Давайте посмотрим, как компонент Select будет работать, используя эту новую технику. Мы аннотируем список элементов / опций с помощью @InjectSelectSupport

public class MyPage {

//Items for Select
@InjectSelectSupport(type=User.class, label="${user} (${address})", index="id")
private List<User> users;

//Value for Select
@Property
private User user;

void onActivate(){
users = load_values_from_database();
}
}

и в шаблоне мы устанавливаем параметры модели и датчика в имя поля опций с добавлением «Поддержка».

<form t:type='form'>
...

<select t:type='select' t:value='user' t:model='usersSupport' t:encoder='usersSupport'>
</select>

...
</form>

шляпа это

Как это работает

Сначала мы создаем универсальные SelectModel и ValueEncoder.

public class SelectSupport<T> extends AbstractSelectModel implements ValueEncoder<T> {
   private final List<T> items;
   private Map<String, PropertyAdapter> adapterMap = new HashMap<String, PropertyAdapter>();
   private String label;
   private PropertyAdapter indexPropertyAdapter;
   private TypeCoercer typeCoercer;
   private final static Pattern PROPERTY_PATTERN = Pattern.compile("\\$\\{([\\w.$]+)\\}");

   public SelectSupport(final List<T> items, final String label,
         String indexProperty,
         final Class<?> valueType,
         final PropertyAccess access, TypeCoercer typeCoercer) {
      this.items = items;
      this.label = label;
      indexPropertyAdapter = access.getAdapter(valueType).getPropertyAdapter(indexProperty);
      Matcher matcher = PROPERTY_PATTERN.matcher(label);
      this.typeCoercer = typeCoercer;
      while (matcher.find()) {
         adapterMap.put(matcher.group(0),
               access.getAdapter(valueType).getPropertyAdapter(matcher.group(1)));
      }
   }

   public List<OptionGroupModel> getOptionGroups() {
      return null;
   }

   public List<OptionModel> getOptions() {
      final List<OptionModel> options = new ArrayList<OptionModel>();

      if (items != null) {
         for(T item: items){
            options.add(new OptionModelImpl(toLabel(item), item));
         }
      }

      return options;
   }

   private String toLabel(Object object) {
      String label = this.label;
      for (String key : adapterMap.keySet()) {
         label = label.replace(key,
               adapterMap.get(key).get(object).toString());
      }
      return label;
   }

   /**
    * {@inheritDoc}
    */
   public String toClient(T value) {
      return typeCoercer.coerce(getIndex(value), String.class);
   }

   /**
    * {@inheritDoc}
    */
   @SuppressWarnings("unchecked")
   public T toValue(String clientValue) {
      for(T value: items){
         if(getIndex(value).equals(typeCoercer.coerce(clientValue, indexPropertyAdapter.getType()))){
            return value;
         }
      }

      return null;
   }

   /**
    * Gets the index value for a particular item
    *
    * @param value
    */
   private Object getIndex(Object value){
      Object fieldValue = indexPropertyAdapter.get(value);
      if(fieldValue == null){
         throw new RuntimeException("Index property cannot be null");
      }
      return fieldValue;
   }
}

Он принимает список элементов, выражение метки и имя свойства индекса в качестве параметров и реализует интерфейс SelectModel и ValueEncoder. Выражение метки может содержать любую комбинацию имен текста и свойств с именами свойств, заключенными в $ {}. Он использует службу PropertyAccess для получения PropertyAdapter для каждого свойства, используемого в выражении метки, а затем использует PropertyAdapter для чтения значений этих свойств. Служба TypeCoercer используется для преобразования между уникальным значением индекса элемента и значением String, переданным в качестве атрибута значения в тег <option>.

Теперь мы можем использовать SelectSupport в нашем компоненте / странице как

@Inject
private PropertyAccess propertyAccess;

@Inject
private TypeCoercer typeCoercer;

private SelectSupport _selectSupport;

public SelectSupport getUsersSupport(){
if(_selectSupport == null){
_selectSupport = new SelectSupport(users,
annotation.label(), annotation.index(), annotation.type(),
propertyAccess, typeCoercer);
}
return _selectSupport;

или мы можем создать преобразование класса. Для этого нам потребуется аннотация

public @interface InjectSelectSupport {
/**
* Expression to be used as label for select model. This takes an
* expression as a parameter. The expression can be any property
* value with <code>${}</code>.
*/
String label();

/**
* Property to be used as index. It's value must be unique for each
* item
*/
String index();

/**
* This suffix is appended to the generated method name.
* Default is <code>Support</code>
*/
String methodSuffix() default "Support";

/**
* Item type.
*/
Class<?> type();

}

Преобразование класса ищет любое поле, помеченное @InjectSelectSupport, и генерирует / вводит метод получения для SelectSupport в компоненте / странице, который затем можно использовать для установки параметров модели и кодировщика компонента Select.

public class InjectSelectSupportWorker implements ComponentClassTransformWorker {

private PropertyAccess propertyAccess;
private TypeCoercer typeCoercer;

public InjectSelectSupportWorker(PropertyAccess propertyAccess, TypeCoercer typeCoercer) {
this.propertyAccess = propertyAccess;
this.typeCoercer = typeCoercer;
}

public void transform(ClassTransformation transform, MutableComponentModel model) {
for (final TransformField field : transform.matchFieldsWithAnnotation(InjectSelectSupport.class)) {
final InjectSelectSupport annotation = field.getAnnotation(InjectSelectSupport.class);

String selectSupportPropertyName = field.getName() + annotation.methodSuffix();
String methodName = "get" + InternalUtils.capitalize(selectSupportPropertyName);

TransformMethodSignature sig = new TransformMethodSignature(Modifier.PUBLIC,
SelectSupport.class.getName(), methodName, null, null);

final TransformMethod method = transform.getOrCreateMethod(sig);

// Add a field to cache the result
final TransformField selectSupportField = transform.createField(Modifier.PRIVATE,
SelectSupport.class.getName(), "_$" + selectSupportPropertyName);
final FieldAccess selectSupportFieldAccess = selectSupportField.getAccess();

final FieldAccess access = field.getAccess();
method.addAdvice(new ComponentMethodAdvice() {

@SuppressWarnings({ "unchecked", "rawtypes" })
public void advise(ComponentMethodInvocation invocation) {
Object instance = invocation.getInstance();
if (selectSupportFieldAccess.read(instance) == null) {
selectSupportFieldAccess.write(
instance,
new SelectSupport((List) access.read(invocation.getInstance()),
annotation.label(), annotation.index(), annotation.type(),
propertyAccess, typeCoercer));
}
invocation.overrideResult(selectSupportFieldAccess.read(instance));
}

});
}
}

 

Наконец, преобразование класса вносится в цепочку ComponentClassTransformWorker.

@Contribute(ComponentClassTransformWorker.class)
public static void provideWorkers(OrderedConfiguration<ComponentClassTransformWorker> workers) {
workers.addInstance("InjectSelectSupport", InjectSelectSupportWorker.class);
}