Java tutorial
/* * Copyright (c) 2008-2016 Haulmont. * * 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 com.haulmont.cuba.gui.data.impl; import com.haulmont.bali.collections.ReadOnlyLinkedMapValuesView; import com.haulmont.chile.core.model.Instance; import com.haulmont.chile.core.model.MetaPropertyPath; import com.haulmont.cuba.client.ClientConfig; import com.haulmont.cuba.core.entity.Entity; import com.haulmont.cuba.core.global.*; import com.haulmont.cuba.core.global.filter.Condition; import com.haulmont.cuba.core.global.filter.DenyingClause; import com.haulmont.cuba.core.global.filter.LogicalCondition; import com.haulmont.cuba.core.global.filter.LogicalOp; import com.haulmont.cuba.gui.components.AggregationInfo; import com.haulmont.cuba.gui.data.CollectionDatasource; import com.haulmont.cuba.gui.logging.UIPerformanceLogger; import com.haulmont.cuba.security.entity.EntityOp; import org.apache.commons.collections4.map.LinkedMap; import org.perf4j.StopWatch; import org.perf4j.slf4j.Slf4JStopWatch; import org.slf4j.LoggerFactory; import java.util.*; import static com.haulmont.bali.util.Preconditions.checkNotNullArgument; /** * Most commonly used {@link CollectionDatasource} implementation. * Contains collection of standalone (not property) entities, and can request data from database and commit changes. * <br> * Can be used as a base class for custom datasources that override e.g. {@link #loadData(java.util.Map)} method. * * @param <T> type of entity * @param <K> type of entity ID */ public class CollectionDatasourceImpl<T extends Entity<K>, K> extends AbstractCollectionDatasource<T, K> implements CollectionDatasource.Indexed<T, K>, CollectionDatasource.Sortable<T, K>, CollectionDatasource.Aggregatable<T, K>, CollectionDatasource.Suspendable<T, K>, CollectionDatasource.SupportsPaging<T, K>, CollectionDatasource.SupportsApplyToSelected<T, K> { protected LinkedMap data = new LinkedMap(); protected boolean inRefresh; protected AggregatableDelegate<K> aggregatableDelegate = new AggregatableDelegate<K>() { @Override public Object getItem(K itemId) { return CollectionDatasourceImpl.this.getItem(itemId); } @Override public Object getItemValue(MetaPropertyPath property, K itemId) { return CollectionDatasourceImpl.this.getItemValue(property, itemId); } }; protected boolean suspended; protected boolean refreshOnResumeRequired; protected int firstResult; protected boolean sortOnDb = AppBeans.<Configuration>get(Configuration.NAME).getConfig(ClientConfig.class) .getCollectionDatasourceDbSortEnabled(); protected LoadContext.Query lastQuery; protected LinkedList<LoadContext.Query> prevQueries = new LinkedList<>(); protected Integer queryKey; @Override public void refreshIfNotSuspended() { if (suspended) { if (!state.equals(State.VALID)) { state = State.VALID; } refreshOnResumeRequired = true; } else { refresh(); } } @Override public void refreshIfNotSuspended(Map<String, Object> parameters) { if (suspended) { if (!state.equals(State.VALID)) { state = State.VALID; } savedParameters = parameters; refreshOnResumeRequired = true; } else { refresh(parameters); } } @Override public void refresh() { if (savedParameters == null) refresh(Collections.emptyMap()); else refresh(savedParameters); } @Override public void refresh(Map<String, Object> parameters) { backgroundWorker.checkUIAccess(); if (inRefresh) return; if (refreshMode == RefreshMode.NEVER) { savedParameters = parameters; invalidate(); State prevState = state; if (!prevState.equals(State.VALID)) { valid(); fireStateChanged(prevState); } inRefresh = true; setItem(getItem()); if (sortInfos != null && sortInfos.length > 0) doSort(); suspended = false; refreshOnResumeRequired = false; fireCollectionChanged(Operation.REFRESH, Collections.emptyList()); inRefresh = false; return; } inRefresh = true; try { Collection prevIds = beforeRefresh(parameters); loadData(parameters); afterRefresh(parameters, prevIds); } finally { inRefresh = false; } } protected Collection beforeRefresh(Map<String, Object> parameters) { savedParameters = parameters; Collection prevIds = data.keySet(); invalidate(); return prevIds; } @SuppressWarnings("unused") protected void afterRefresh(Map<String, Object> parameters, Collection prevIds) { State prevState = state; if (!prevState.equals(State.VALID)) { state = State.VALID; fireStateChanged(prevState); } if (this.item != null && !prevIds.contains(this.item.getId())) { setItem(null); } else if (this.item != null) { setItem(getItem(this.item.getId())); } else { setItem(null); } if (sortInfos != null && sortInfos.length > 0) doSort(); suspended = false; refreshOnResumeRequired = false; fireCollectionChanged(Operation.REFRESH, Collections.emptyList()); checkDataLoadError(); } @Override public T getItem(K id) { backgroundWorker.checkUIAccess(); if (state == State.NOT_INITIALIZED) { throw new IllegalStateException("Invalid datasource state " + state); } else { T item = (T) data.get(id); return item; } } @Override public Collection<K> getItemIds() { backgroundWorker.checkUIAccess(); if (state == State.NOT_INITIALIZED) { return Collections.emptyList(); } else { return (Collection<K>) data.keySet(); } } @Override public Collection<T> getItems() { backgroundWorker.checkUIAccess(); if (state == State.NOT_INITIALIZED) { return Collections.emptyList(); } else { //noinspection unchecked return new ReadOnlyLinkedMapValuesView(data); } } @Override public int size() { backgroundWorker.checkUIAccess(); if ((state == State.NOT_INITIALIZED) || suspended) { return 0; } else { return data.size(); } } @Override public void sort(SortInfo[] sortInfos) { if (sortInfos.length != 1) { throw new UnsupportedOperationException("Supporting sort by one field only"); } //noinspection unchecked this.sortInfos = sortInfos; if (data.size() > 0) { if (!sortOnDb || containsAllDataFromDb()) { doSort(); fireCollectionChanged(Operation.REFRESH, Collections.emptyList()); } else { refresh(); } } } @Override public void resetSortOrder() { this.sortInfos = null; } protected boolean containsAllDataFromDb() { return firstResult == 0 && data.size() < maxResults; } protected void doSort() { @SuppressWarnings("unchecked") List<T> list = new ArrayList<>(data.values()); list.sort(createEntityComparator()); data.clear(); for (T t : list) { data.put(t.getId(), t); } } @Override public int indexOfId(K itemId) { return data.indexOf(itemId); } @Override public K getIdByIndex(int index) { if (!data.isEmpty()) { return (K) data.get(index); } return null; } @Override public List<K> getItemIds(int startIndex, int numberOfItems) { //noinspection unchecked return data.asList().subList(startIndex, startIndex + numberOfItems); } @Override public K firstItemId() { if (!data.isEmpty()) { return (K) data.firstKey(); } return null; } @Override public K lastItemId() { if (!data.isEmpty()) { return (K) data.lastKey(); } return null; } @Override public K nextItemId(K itemId) { return (K) data.nextKey(itemId); } @Override public K prevItemId(K itemId) { return (K) data.previousKey(itemId); } @Override public boolean isFirstId(K itemId) { return itemId != null && itemId.equals(firstItemId()); } @Override public boolean isLastId(K itemId) { return itemId != null && itemId.equals(lastItemId()); } protected void checkState() { if (state != State.VALID) { refresh(); } } protected void checkStateBeforeAdd() { if (state != State.VALID || isSuspended()) { this.suspended = false; refresh(); } } @Override public void addItem(T item) { checkNotNullArgument(item, "item is null"); internalAddItem(item, () -> { data.put(item.getId(), item); }); } @Override public void addItemFirst(T item) { checkNotNullArgument(item, "item is null"); internalAddItem(item, () -> { LinkedMap tmpMap = (LinkedMap) data.clone(); data.clear(); data.put(item.getId(), item); data.putAll(tmpMap); }); } protected void internalAddItem(T item, Runnable addToData) { checkNotNullArgument(item, "item is null"); backgroundWorker.checkUIAccess(); checkStateBeforeAdd(); addToData.run(); attachListener(item); modified(item); fireCollectionChanged(Operation.ADD, Collections.singletonList(item)); } @Override public void removeItem(T item) { checkNotNullArgument(item, "item is null"); backgroundWorker.checkUIAccess(); checkState(); if (this.item != null && this.item.equals(item)) { setItem(null); } data.remove(item.getId()); detachListener(item); deleted(item); fireCollectionChanged(Operation.REMOVE, Collections.singletonList(item)); } @Override public void includeItem(T item) { checkNotNullArgument(item, "item is null"); internalIncludeItem(item, () -> { data.put(item.getId(), item); }); } @Override public void includeItemFirst(T item) { checkNotNullArgument(item, "item is null"); internalIncludeItem(item, () -> { LinkedMap tmpMap = (LinkedMap) data.clone(); data.clear(); data.put(item.getId(), item); data.putAll(tmpMap); }); } protected void internalIncludeItem(T item, Runnable addToData) { backgroundWorker.checkUIAccess(); checkStateBeforeAdd(); addToData.run(); attachListener(item); fireCollectionChanged(Operation.ADD, Collections.singletonList(item)); } @Override public void excludeItem(T item) { checkNotNullArgument(item, "item is null"); backgroundWorker.checkUIAccess(); checkState(); if (this.item != null && this.item.equals(item)) { setItem(null); } data.remove(item.getId()); detachListener(item); fireCollectionChanged(Operation.REMOVE, Collections.singletonList(item)); } @Override public void clear() { backgroundWorker.checkUIAccess(); // replaced refresh call with state initialization if (state != State.VALID) { invalidate(); State prevState = state; if (prevState != State.VALID) { valid(); fireStateChanged(prevState); } } // Get items List<Object> collectionItems = new ArrayList<>(data.values()); // Clear container data.clear(); // Notify listeners for (Object obj : collectionItems) { T item = (T) obj; detachListener(item); } setItem(null); fireCollectionChanged(Operation.CLEAR, Collections.emptyList()); } @Override public void revert() { if (refreshMode != RefreshMode.NEVER) { refresh(); } else { clear(); } } @Override public void modifyItem(T item) { checkNotNullArgument(item, "item is null"); if (data.containsKey(item.getId())) { if (PersistenceHelper.isNew(item)) { Object existingItem = data.get(item.getId()); metadata.getTools().copy(item, (Instance) existingItem); modified((T) existingItem); } else { updateItem(item); modified(item); } } } @Override public void updateItem(T item) { checkNotNullArgument(item, "item is null"); backgroundWorker.checkUIAccess(); checkState(); if (this.item != null && this.item.equals(item)) { T prevItem = item; this.item = item; fireItemChanged(prevItem); } if (data.containsKey(item.getId())) { data.put(item.getId(), item); attachListener(item); fireCollectionChanged(Operation.UPDATE, Collections.singletonList(item)); } } @Override public boolean containsItem(K itemId) { return data.containsKey(itemId); } @SuppressWarnings("unchecked") @Override public void committed(Set<Entity> entities) { if (!State.VALID.equals(state)) { return; } for (Entity entity : entities) { if (!metaClass.getJavaClass().isAssignableFrom(entity.getClass())) continue; if (entity.equals(item)) { item = (T) entity; } updateItem((T) entity); } modified = false; clearCommitLists(); } protected boolean needLoading() { if (filter != null) { if (filter.getRoot() instanceof DenyingClause) { return false; } if ((filter.getRoot() instanceof LogicalCondition) && ((LogicalCondition) filter.getRoot()).getOperation().equals(LogicalOp.AND)) { for (Condition condition : filter.getRoot().getConditions()) { if (condition instanceof DenyingClause) { return false; } } } } return true; } @Override public LoadContext getCompiledLoadContext() { LoadContext context = new LoadContext(metaClass); Map<String, Object> params; if (savedParameters == null) { params = Collections.emptyMap(); } else params = savedParameters; LoadContext.Query q = (LoadContext.Query) createDataQuery(context, params); if (sortInfos != null && sortOnDb) { setSortDirection(q); } context.setView(view); context.setSoftDeletion(softDeletion); prepareLoadContext(context); return context; } /** * Load data from middleware into {@link #data} field. * <p>In case of error sets {@link #dataLoadError} field to the exception object.</p> * @param params datasource parameters, as described in {@link CollectionDatasource#refresh(java.util.Map)} */ protected void loadData(Map<String, Object> params) { Security security = AppBeans.get(Security.NAME); if (!security.isEntityOpPermitted(metaClass, EntityOp.READ)) { return; } String tag = getLoggingTag("CDS"); StopWatch sw = new Slf4JStopWatch(tag, LoggerFactory.getLogger(UIPerformanceLogger.class)); if (needLoading()) { LoadContext context = beforeLoadData(params); if (context == null) { return; } try { final Collection<T> entities = dataSupplier.loadList(context); afterLoadData(params, context, entities); } catch (Throwable e) { dataLoadError = e; } } sw.stop(); } /** * This method is invoked by {@link #loadData(Map)} method immediately before loading entities from {@code DataSupplier}. * <br>If you override this method, be sure to call {@code super()}. * * @param params datasource parameters, as described in {@link CollectionDatasource#refresh(java.util.Map)} * @return LoadContext which will be used to load data */ protected LoadContext beforeLoadData(Map<String, Object> params) { final LoadContext context = new LoadContext(metaClass); LoadContext.Query q = (LoadContext.Query) createDataQuery(context, params); if (q == null) { detachListener(data.values()); data.clear(); return null; } if (sortInfos != null && sortOnDb) { setSortDirection(q); } if (firstResult > 0) q.setFirstResult(firstResult); if (maxResults > 0) { q.setMaxResults(maxResults); } context.setView(view); context.setSoftDeletion(isSoftDeletion()); prepareLoadContext(context); dataLoadError = null; return context; } /** * This method is invoked by {@link #loadData(Map)} method immediately after loading entities from {@code DataSupplier}. * <p>If you override this method, be sure to call {@code super()}. If you process the loaded entities somehow, * call {@code super()} after processing. * * @param params datasource parameters, as described in {@link CollectionDatasource#refresh(java.util.Map)} * @param context {@code LoadContext} which was used for loading data * @param entities loaded entities */ protected void afterLoadData(@SuppressWarnings("unused") Map<String, Object> params, LoadContext context, Collection<T> entities) { detachListener(data.values()); data.clear(); for (T entity : entities) { data.put(entity.getId(), entity); attachListener(entity); } lastQuery = context.getQuery(); } @Override protected void prepareLoadContext(LoadContext context) { context.setLoadDynamicAttributes(loadDynamicAttributes); context.setQueryKey(queryKey == null ? 0 : queryKey); context.getPrevQueries().addAll(prevQueries); } protected void detachListener(Collection instances) { for (Object obj : instances) { if (obj instanceof Instance) detachListener((Instance) obj); } } @Override @SuppressWarnings("unchecked") public Map<AggregationInfo, String> aggregate(AggregationInfo[] aggregationInfos, Collection itemIds) { return aggregatableDelegate.aggregate(aggregationInfos, itemIds); } protected Object getItemValue(MetaPropertyPath property, K itemId) { Instance instance = getItemNN(itemId); if (property.getMetaProperties().length == 1) { return instance.getValue(property.getMetaProperty().getName()); } else { return instance.getValueEx(property.toString()); } } @Override public boolean isSuspended() { return suspended; } @Override public void setSuspended(boolean suspended) { boolean wasSuspended = this.suspended; this.suspended = suspended; if (wasSuspended && !suspended && refreshOnResumeRequired) { refresh(); } } @Override public int getFirstResult() { return firstResult; } @Override public void setFirstResult(int startPosition) { this.firstResult = startPosition; } protected void incrementQueryKey() { queryKey = userSession.getAttribute("_queryKey"); if (queryKey == null) queryKey = 1; else queryKey++; userSession.setAttribute("_queryKey", queryKey); } @Override public void pinQuery() { if (prevQueries.isEmpty()) incrementQueryKey(); if (lastQuery != null) prevQueries.add(lastQuery); } @Override public void unpinLastQuery() { if (!prevQueries.isEmpty()) { prevQueries.removeLast(); } } @Override public void unpinAllQuery() { if (!prevQueries.isEmpty()) { prevQueries.clear(); } } }