package com.constellio.app.ui.framework.components.fields.lookup; import static com.constellio.app.ui.i18n.i18n.$; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Locale; import org.apache.commons.lang3.StringUtils; import org.vaadin.addons.lazyquerycontainer.LazyQueryContainer; import org.vaadin.addons.lazyquerycontainer.LazyQueryDefinition; import org.vaadin.addons.lazyquerycontainer.Query; import org.vaadin.addons.lazyquerycontainer.QueryDefinition; import org.vaadin.addons.lazyquerycontainer.QueryFactory; import com.constellio.app.ui.application.ConstellioUI; import com.constellio.app.ui.framework.buttons.DeleteButton; import com.constellio.app.ui.framework.buttons.WindowButton; import com.constellio.app.ui.framework.components.fields.BaseTextField; import com.constellio.app.ui.framework.components.fields.autocomplete.BaseAutocompleteField; import com.constellio.app.ui.framework.components.fields.autocomplete.BaseAutocompleteField.AutocompleteSuggestionsProvider; import com.constellio.app.ui.framework.components.tree.LazyTree; import com.constellio.app.ui.framework.data.AbstractDataProvider; import com.constellio.app.ui.framework.data.LazyTreeDataProvider; import com.constellio.app.ui.framework.data.RecordLookupTreeDataProvider; import com.constellio.app.ui.handlers.OnEnterKeyHandler; import com.constellio.app.ui.pages.base.PresenterService; import com.constellio.model.entities.records.wrappers.User; import com.constellio.model.services.factories.ModelLayerFactory; import com.vaadin.data.Container; import com.vaadin.data.Item; import com.vaadin.data.Property; import com.vaadin.data.Validator.InvalidValueException; import com.vaadin.data.util.ObjectProperty; import com.vaadin.data.util.converter.Converter; import com.vaadin.event.ItemClickEvent; import com.vaadin.event.ItemClickEvent.ItemClickListener; import com.vaadin.server.ErrorMessage; import com.vaadin.server.ThemeResource; import com.vaadin.ui.AbstractSelect.ItemCaptionMode; import com.vaadin.ui.Button; import com.vaadin.ui.Button.ClickEvent; import com.vaadin.ui.Button.ClickListener; import com.vaadin.ui.Component; import com.vaadin.ui.CustomField; import com.vaadin.ui.HorizontalLayout; import com.vaadin.ui.Panel; import com.vaadin.ui.TabSheet; import com.vaadin.ui.Table; import com.vaadin.ui.Table.ColumnHeaderMode; import com.vaadin.ui.TextField; import com.vaadin.ui.VerticalLayout; import com.vaadin.ui.Window; import com.vaadin.ui.themes.ValoTheme; public abstract class LookupField<T extends Serializable> extends CustomField<T> { public static final String STYLE_NAME = "lookup"; public static final String ERROR_STYLE_NAME = STYLE_NAME + "-error"; public static final String AUTOCOMPLETE_FIELD_STYLE_NAME = STYLE_NAME + "-autocomplete-field"; public static final String OPEN_WINDOW_BUTTON_STYLE_NAME = STYLE_NAME + "-open-window-button"; public static final String CLEAR_BUTTON_STYLE_NAME = STYLE_NAME + "-clear-button"; public static final String LOOKUP_WINDOW_STYLE_NAME = STYLE_NAME + "-window"; public static final String LOOKUP_WINDOW_CONTENT_STYLE_NAME = LOOKUP_WINDOW_STYLE_NAME + "-content"; private static final String CAPTION_PROPERTY_ID = "caption"; private TextInputDataProvider<T> suggestInputDataProvider; private List<LookupTreeDataProvider<T>> lookupTreeDataProviders = new ArrayList<>(); private BaseAutocompleteField<T> autoCompleteField; private WindowButton lookupWindowButton; private Button clearButton; private Converter<String, T> itemConverter; private int treeBufferSize = 100; /** * The component should receive focus (if {@link Focusable}) when attached. */ private boolean delayedFocus; private Integer windowZIndex; @SuppressWarnings("unchecked") public LookupField( TextInputDataProvider<T> suggestInputDataProvider, LookupTreeDataProvider<T>... lookupTreeDataProviders) { this.suggestInputDataProvider = suggestInputDataProvider; if (lookupTreeDataProviders != null) { for (LookupTreeDataProvider<T> lookupTreeDataProvider : lookupTreeDataProviders) { this.lookupTreeDataProviders.add(lookupTreeDataProvider); } } } public final Integer getWindowZIndex() { return windowZIndex; } public final void setWindowZIndex(Integer windowZIndex) { this.windowZIndex = windowZIndex; } public int getTreeBufferSize() { return treeBufferSize; } public void setTreeBufferSize(int treeBufferSize) { this.treeBufferSize = treeBufferSize; } @Override protected Component initContent() { addStyleName(STYLE_NAME); setSizeFull(); final int autoCompleteBuffer = 100; AutocompleteSuggestionsProvider<T> suggestionsProvider = new AutocompleteSuggestionsProvider<T>() { @Override public List<T> suggest(String text) { List<T> values = new ArrayList<>(suggestInputDataProvider.getData(text, 0, autoCompleteBuffer)); if (itemConverter != null) { Collections.sort(values, new Comparator<T>() { @Override public int compare(T o1, T o2) { String s1 = itemConverter.convertToPresentation(o1, String.class, getLocale()); String s2 = itemConverter.convertToPresentation(o2, String.class, getLocale()); return s1.compareTo(s2); } }); } return values; } @Override public int getBufferSize() { return autoCompleteBuffer; } }; autoCompleteField = newAutocompleteField(suggestionsProvider); autoCompleteField.addStyleName(AUTOCOMPLETE_FIELD_STYLE_NAME); autoCompleteField.setItemConverter(itemConverter); autoCompleteField.setPropertyDataSource(this); if (delayedFocus) { autoCompleteField.focus(); } lookupWindowButton = new WindowButton(null, $("search")) { @Override protected Component buildWindowContent() { return new LookupWindowContent(getWindow()); } }; lookupWindowButton.setIcon(new ThemeResource("images/icons/actions/view.png")); lookupWindowButton.addStyleName(ValoTheme.BUTTON_ICON_ONLY); lookupWindowButton.addStyleName(OPEN_WINDOW_BUTTON_STYLE_NAME); lookupWindowButton.setZIndex(windowZIndex); addValueChangeListener(new ValueChangeListener() { @Override public void valueChange(Property.ValueChangeEvent event) { if (lookupWindowButton.getWindow() != null) { Window lookupWindow = lookupWindowButton.getWindow(); lookupWindow.close(); } } }); clearButton = new Button(DeleteButton.ICON_RESOURCE); clearButton.addStyleName(ValoTheme.BUTTON_ICON_ONLY); clearButton.addStyleName(CLEAR_BUTTON_STYLE_NAME); clearButton.addClickListener(new ClickListener() { @Override public void buttonClick(ClickEvent event) { autoCompleteField.setValue(null); } }); HorizontalLayout mainLayout = new HorizontalLayout(autoCompleteField, lookupWindowButton, clearButton); mainLayout.setExpandRatio(autoCompleteField, 1); mainLayout.setSpacing(true); mainLayout.setWidth("100%"); return mainLayout; } protected BaseAutocompleteField<T> newAutocompleteField(AutocompleteSuggestionsProvider<T> suggestionsProvider) { return new BaseAutocompleteField<>(suggestionsProvider); } @SuppressWarnings("unchecked") protected LazyTree<T> newLazyTree(LookupTreeDataProvider<T> lookupTreeDataProvider, int treeBufferSize) { return new LazyTree<T>(lookupTreeDataProvider, treeBufferSize) { @Override public String getItemCaption(T itemId) { return LookupField.this.getCaption(itemId); } @Override public void addItemClickListener(ItemClickListener listener) { super.addItemClickListener(listener); } }; } public Converter<String, T> getItemConverter() { return itemConverter; } public void setItemConverter(Converter<String, T> itemConverter) { this.itemConverter = itemConverter; } protected String getCaption(T object) { String caption; if (object != null) { if (itemConverter != null) { Locale locale = ConstellioUI.getCurrentSessionContext().getCurrentLocale(); caption = itemConverter.convertToPresentation(object, String.class, locale); } else { caption = object.toString(); } } else { caption = ""; } return caption; } protected String getCaptionForLazyTree(LazyTreeDataProvider<T> lazyTreeDataProvider) { return lazyTreeDataProvider.toString(); } @Override public String getRequiredError() { return autoCompleteField.getRequiredError(); } @Override public void setRequiredError(String requiredMessage) { autoCompleteField.setRequiredError(requiredMessage); addStyleName(ERROR_STYLE_NAME); } @Override public String getConversionError() { return autoCompleteField.getConversionError(); } @Override public void setConversionError(String valueConversionError) { autoCompleteField.setConversionError(valueConversionError); addStyleName(ERROR_STYLE_NAME); } @Override public ErrorMessage getComponentError() { return autoCompleteField.getComponentError(); } @Override public void setComponentError(ErrorMessage componentError) { autoCompleteField.setComponentError(componentError); } @Override public void validate() throws InvalidValueException { try { if (autoCompleteField != null) { autoCompleteField.validate(); } super.validate(); removeStyleName(ERROR_STYLE_NAME); } catch (InvalidValueException e) { throw e; } } @Override public void focus() { super.focus(); if (autoCompleteField != null) { autoCompleteField.focus(); delayedFocus = false; } else { delayedFocus = true; } } public void setIgnoreLinkability(boolean ignoreLinkability) { for (LookupTreeDataProvider<?> provider : lookupTreeDataProviders) { if (provider instanceof RecordLookupTreeDataProvider) { ((RecordLookupTreeDataProvider) provider).setIgnoreLinkability(ignoreLinkability); } } } public void setOnlyLinkables(boolean onlyLinkables) { suggestInputDataProvider.setOnlyLinkables(onlyLinkables); } protected class LookupWindowContent extends VerticalLayout { private HorizontalLayout searchFieldLayout; private TextField searchField; private Button searchButton; private Component lookupTreeComponent; private Table searchResultsTable; @SuppressWarnings("unchecked") public LookupWindowContent(Window window) { super(); setSizeFull(); window.setWidth("80%"); window.setHeight("80%"); window.setResizable(true); window.addStyleName(LOOKUP_WINDOW_STYLE_NAME); addStyleName(LOOKUP_WINDOW_CONTENT_STYLE_NAME); setSpacing(true); searchFieldLayout = new HorizontalLayout(); searchFieldLayout.setWidthUndefined(); searchFieldLayout.setSpacing(true); searchField = new BaseTextField(); searchField.focus(); OnEnterKeyHandler onEnterKeyHandler = new OnEnterKeyHandler() { @Override public void onEnterKeyPressed() { search(); } }; onEnterKeyHandler.installOn(searchField); searchButton = new Button($("search")); searchButton.addStyleName(ValoTheme.BUTTON_PRIMARY); searchButton.addClickListener(new ClickListener() { @Override public void buttonClick(ClickEvent event) { search(); } }); if (!getLookupTreeDataProviders().isEmpty()) { if (getLookupTreeDataProviders().size() > 1) { lookupTreeComponent = new TabSheet(); } for (final LookupTreeDataProvider<T> lookupTreeDataProvider : getLookupTreeDataProviders()) { LazyTree<T> lazyTree = newLazyTree(lookupTreeDataProvider, treeBufferSize); lazyTree.setWidth("100%"); lazyTree.setItemCaptionMode(ItemCaptionMode.PROPERTY); lazyTree.setItemCaptionPropertyId(CAPTION_PROPERTY_ID); lazyTree.addItemClickListener(new ItemClickListener() { @Override public void itemClick(ItemClickEvent event) { T objectClicked = (T) event.getItemId(); if (lookupTreeDataProvider.isSelectable(objectClicked)) { LookupField.this.setValue(objectClicked); } } }); Panel lazyTreePanel = new Panel(lazyTree); lazyTreePanel.setWidth("100%"); lazyTreePanel.setHeight("100%"); if (lookupTreeComponent == null) { lookupTreeComponent = lazyTreePanel; } else { TabSheet tabSheet = (TabSheet) lookupTreeComponent; String lazyTreeCaption = getCaptionForLazyTree(lookupTreeDataProvider); tabSheet.addTab(lazyTreePanel, lazyTreeCaption); selectDefaultUserTaxonomyTab(lazyTree, tabSheet, lazyTreePanel); } } } searchResultsTable = new Table(); searchResultsTable.setWidth("100%"); searchResultsTable.setHeight("98%"); searchResultsTable.setColumnHeaderMode(ColumnHeaderMode.HIDDEN); addComponent(searchFieldLayout); if (!getLookupTreeDataProviders().isEmpty()) { addComponent(lookupTreeComponent); } else { Container searchResultsContainer = new LookupSearchResultContainer(geSuggestInputDataProvider(), searchField); searchResultsTable.setContainerDataSource(searchResultsContainer); addComponent(searchResultsTable); } searchFieldLayout.addComponents(searchField, searchButton); if (!getLookupTreeDataProviders().isEmpty()) { setExpandRatio(lookupTreeComponent, 1); } else { setExpandRatio(searchResultsTable, 1); } searchFieldLayout.setExpandRatio(searchField, 1); } @SuppressWarnings("unchecked") private LookupTreeDataProvider<T> getCurrentTreeDataProvider() { Panel currentLazyTreePanel; if (lookupTreeComponent instanceof TabSheet) { TabSheet tabSheet = (TabSheet) lookupTreeComponent; currentLazyTreePanel = (Panel) tabSheet.getSelectedTab(); } else { currentLazyTreePanel = (Panel) lookupTreeComponent; } LazyTree<T> currentLazyTree = (LazyTree<T>) currentLazyTreePanel.getContent(); return (LookupTreeDataProvider<T>) currentLazyTree.getDataProvider(); } public List<LookupTreeDataProvider<T>> getLookupTreeDataProviders() { return lookupTreeDataProviders; } private void search() { String text = searchField.getValue(); if (lookupTreeComponent != null) { if (StringUtils.isNotBlank(text)) { if (lookupTreeComponent.isVisible()) { lookupTreeComponent.setVisible(false); searchResultsTable.setVisible(true); } LookupTreeDataProvider<T> currentDataProvider = getCurrentTreeDataProvider(); Container searchResultsContainer = new LookupSearchResultContainer(currentDataProvider.search(), searchField); searchResultsTable.setContainerDataSource(searchResultsContainer); replaceComponent(lookupTreeComponent, searchResultsTable); setExpandRatio(searchResultsTable, 1); } else { lookupTreeComponent.setVisible(true); searchResultsTable.setVisible(false); replaceComponent(searchResultsTable, lookupTreeComponent); setExpandRatio(lookupTreeComponent, 1); } } else { Container searchResultsContainer = new LookupSearchResultContainer(geSuggestInputDataProvider(), searchField); searchResultsTable.setContainerDataSource(searchResultsContainer); } } public TextInputDataProvider geSuggestInputDataProvider() { return suggestInputDataProvider; } } private void selectDefaultUserTaxonomyTab(LazyTree<T> lazyTree, TabSheet tabSheet, Panel lazyTreePanel) { User user = suggestInputDataProvider.getCurrentUser(); String taxonomyCode = lazyTree.getDataProvider().getTaxonomyCode(); String userDefaultTaxonomy = user.getDefaultTaxonomy(); PresenterService presenterService = new PresenterService(suggestInputDataProvider.getModelLayerFactory()); String configDefaultTaxonomy = presenterService.getSystemConfigs().getDefaultTaxonomy(); if (userDefaultTaxonomy != null) { if (taxonomyCode.equals(userDefaultTaxonomy)) { tabSheet.setSelectedTab(lazyTreePanel); } } else if (taxonomyCode.equals(configDefaultTaxonomy)) { tabSheet.setSelectedTab(lazyTreePanel); } } public interface LookupTreeDataProvider<T extends Serializable> extends LazyTreeDataProvider<T> { TextInputDataProvider<T> search(); boolean isSelectable(T selection); } public static abstract class TextInputDataProvider<T> extends AbstractDataProvider { public abstract List<T> getData(String text, int startIndex, int count); public abstract ModelLayerFactory getModelLayerFactory(); public abstract int size(String text); public abstract User getCurrentUser(); public abstract void setOnlyLinkables(boolean onlyLinkables); } private class LookupSearchResultContainer extends LazyQueryContainer { public LookupSearchResultContainer(TextInputDataProvider<T> lookupData, Property<String> property) { super(new LookupSearchResultLazyQueryDefinition(), new LookupSearchResultLazyQueryFactory(lookupData, property)); } } private class LookupSearchResultLazyQueryDefinition extends LazyQueryDefinition { public LookupSearchResultLazyQueryDefinition() { super(true, 100, null); addProperty(CAPTION_PROPERTY_ID, Button.class, null, true, false); } } private class LookupSearchResultLazyQueryFactory implements QueryFactory, Serializable { private TextInputDataProvider<T> lookupData; private Property<String> property; public LookupSearchResultLazyQueryFactory(TextInputDataProvider<T> lookupData, Property<String> property) { this.lookupData = lookupData; this.property = property; } @Override public Query constructQuery(final QueryDefinition queryDefinition) { return new SerializableQuery() { @Override public int size() { int size; String text = property.getValue(); // if (StringUtils.isNotBlank(text)) { size = lookupData.size(text); // } else { // size = 0; // } return size; } @Override public List<Item> loadItems(int startIndex, int count) { List<Item> dataItems = new ArrayList<Item>(); String text = property.getValue(); // if (StringUtils.isNotBlank(text)) { List<T> dataObjects = lookupData.getData(text, startIndex, count); for (T dataObject : dataObjects) { Item dataItem = new DataItem(dataObject); dataItems.add(dataItem); } // } return dataItems; } @Override public void saveItems(List<Item> addedItems, List<Item> modifiedItems, List<Item> removedItems) { throw new UnsupportedOperationException("Query is read-only"); } @Override public boolean deleteAllItems() { throw new UnsupportedOperationException("Query is read-only"); } @Override public Item constructItem() { throw new UnsupportedOperationException("Query is read-only"); } }; } } private class DataItem implements Item { private T item; public DataItem(T item) { this.item = item; } @Override public Property<?> getItemProperty(Object id) { Property<?> property; if (CAPTION_PROPERTY_ID.equals(id)) { String caption = getCaption(item); Button selectButton = new Button(caption); selectButton.addClickListener(new ClickListener() { @Override public void buttonClick(ClickEvent event) { LookupField.this.setValue(item); } }); selectButton.addStyleName(ValoTheme.BUTTON_LINK); property = new ObjectProperty<Button>(selectButton); } else { property = null; } return property; } @Override public Collection<?> getItemPropertyIds() { return Arrays.asList(CAPTION_PROPERTY_ID); } @SuppressWarnings("rawtypes") @Override public boolean addItemProperty(Object id, Property property) throws UnsupportedOperationException { throw new UnsupportedOperationException("Item is read-only"); } @Override public boolean removeItemProperty(Object id) throws UnsupportedOperationException { throw new UnsupportedOperationException("Item is read-only"); } } private interface SerializableQuery extends Query, Serializable { } }