DefaultTableFormat.java
001 /*
002  * Copyright 2008-2017 the original author or authors.
003  *
004  * Licensed under the Apache License, Version 2.0 (the "License");
005  * you may not use this file except in compliance with the License.
006  * You may obtain a copy of the License at
007  *
008  *     http://www.apache.org/licenses/LICENSE-2.0
009  *
010  * Unless required by applicable law or agreed to in writing, software
011  * distributed under the License is distributed on an "AS IS" BASIS,
012  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013  * See the License for the specific language governing permissions and
014  * limitations under the License.
015  */
016 package griffon.javafx.scene.control;
017 
018 import javafx.beans.value.ObservableValue;
019 
020 import javax.annotation.Nonnull;
021 import javax.annotation.Nullable;
022 import java.lang.reflect.InvocationTargetException;
023 import java.lang.reflect.Method;
024 import java.util.Arrays;
025 import java.util.LinkedHashMap;
026 import java.util.Map;
027 
028 import static griffon.util.GriffonClassUtils.requireState;
029 import static griffon.util.GriffonNameUtils.getNaturalName;
030 import static griffon.util.GriffonNameUtils.requireNonBlank;
031 import static java.lang.System.arraycopy;
032 import static java.util.Objects.requireNonNull;
033 
034 /**
035  @author Andres Almiray
036  @since 2.11.0
037  */
038 public class DefaultTableFormat<E> implements TableViewFormat<E> {
039     public static class Column<E,T> {
040         private final String name;
041         private final String title;
042         private final Double size;
043         private final TableCellFactory<E,T> tableCellFactory;
044 
045         public Column(@Nonnull String name) {
046             this(name, getNaturalName(name), null, null);
047         }
048 
049         public Column(@Nonnull String name, @Nonnull String title) {
050             this(name, title, null, null);
051         }
052 
053         public Column(@Nonnull String name, @Nonnull Double size) {
054             this(name, getNaturalName(name), size, null);
055         }
056 
057         public Column(@Nonnull String name, @Nonnull TableCellFactory<E,T> tableCellFactory) {
058             this(name, getNaturalName(name), null, tableCellFactory);
059         }
060 
061         public Column(@Nonnull String name, @Nonnull String title, @Nonnull Double size) {
062             this(name, title, size, null);
063         }
064 
065         public Column(@Nonnull String name, @Nonnull String title, @Nonnull TableCellFactory<E,T> tableCellFactory) {
066             this(name, title, null, tableCellFactory);
067         }
068 
069         public Column(@Nonnull String name, @Nonnull Double size, @Nonnull TableCellFactory<E,T> tableCellFactory) {
070             this(name, getNaturalName(name), size, tableCellFactory);
071         }
072 
073         public Column(@Nonnull String name, @Nonnull String title, @Nullable Double size, @Nonnull TableCellFactory<E,T> tableCellFactory) {
074             this.name = requireNonBlank(name, "Argument 'name' must not be blank");
075             this.title = requireNonBlank(title, "Argument 'title' must not be blank");
076             if (size != null) {
077                 requireState(size > 0"Argument 'size' must be greater than 0.0d");
078                 requireState(size <= 1"Argument 'size' must be less than or equal to 1.0d");
079             }
080             this.size = size;
081             this.tableCellFactory = tableCellFactory;
082         }
083 
084         @Nonnull
085         public String getName() {
086             return name;
087         }
088 
089         @Nonnull
090         public String getTitle() {
091             return title;
092         }
093 
094         @Nullable
095         public Double getSize() {
096             return size;
097         }
098 
099         @Nullable
100         public TableCellFactory<E,T> getTableCellFactory() {
101             return tableCellFactory;
102         }
103     }
104 
105     private final Column[] columns;
106     private final Map<Class<?>, Map<String, Method>> observableMetadata = new LinkedHashMap<>();
107 
108     public DefaultTableFormat(@Nonnull String... names) {
109         requireNonNull(names, "Argument 'names' must not be null");
110         requireState(names.length > 0"Column size must be greater than zero");
111         this.columns = new Column[names.length];
112         for (int i = 0; i < names.length; i++) {
113             this.columns[inew Column(names[i]);
114         }
115     }
116 
117     public DefaultTableFormat(@Nonnull Column... columns) {
118         requireNonNull(columns, "Argument 'columns' must not be null");
119         requireState(columns.length > 0"Column size must be greater than zero");
120         this.columns = new Column[columns.length];
121         arraycopy(columns, 0this.columns, 0, columns.length);
122     }
123 
124     @Override
125     public int getColumnCount() {
126         return columns.length;
127     }
128 
129     @Nonnull
130     @Override
131     public String getColumnName(int index) {
132         return columns[index].getTitle();
133     }
134 
135     @Nullable
136     @Override
137     public Double getColumnSize(int index) {
138         return columns[index].getSize();
139     }
140 
141     @Nonnull
142     @Override
143     public ObservableValue<?> getObservableValue(@Nonnull E instance, int index) {
144         final String columnName = columns[index].getName();
145         Class<?> klass = instance.getClass();
146         Map<String, Method> metadata = observableMetadata.computeIfAbsent(klass, this::harvestMetadata);
147 
148         try {
149             Method method = metadata.get(columnName);
150             if (method == null) {
151                 throw new IllegalStateException("Could not find a method in " + klass +
152                     " returning " + ObservableValue.class.getSimpleName() " associated with column " + columnName);
153             }
154             return (ObservableValue<?>method.invoke(instance);
155         catch (IllegalAccessException e) {
156             throw new IllegalStateException(e);
157         catch (InvocationTargetException e) {
158             throw new IllegalStateException(e.getTargetException());
159         }
160     }
161 
162     @Nullable
163     @Override
164     public TableCellFactory getTableCellFactory(int index) {
165         return columns[index].getTableCellFactory();
166     }
167 
168     @Nonnull
169     private Map<String, Method> harvestMetadata(@Nonnull Class<?> klass) {
170         Map<String, Method> map = new LinkedHashMap<>();
171 
172         for (Method method : klass.getMethods()) {
173             if (ObservableValue.class.isAssignableFrom(method.getReturnType()) &&
174                 method.getParameterCount() == 0) {
175                 Arrays.stream(columns)
176                     .map(Column::getName)
177                     .filter(name -> method.getName().startsWith(name))
178                     .findFirst()
179                     .ifPresent(name -> map.put(name, method));
180             }
181         }
182 
183         return map;
184     }
185 }