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