All Downloads are FREE. Search and download functionalities are using the official Maven repository.

xdev.ui.ItemList Maven / Gradle / Ivy

/*
 * XDEV Application Framework - XDEV Application Framework
 * Copyright © 2003 XDEV Software (https://xdev.software)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see .
 */
package xdev.ui;


import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import javax.swing.Icon;
import javax.swing.ListModel;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;

import xdev.db.QueryInfo;
import xdev.db.sql.Condition;
import xdev.db.sql.SELECT;
import xdev.db.sql.WHERE;
import xdev.lang.Copyable;
import xdev.lang.NotNull;
import xdev.ui.paging.ItemListPageControl;
import xdev.util.ArrayUtils;
import xdev.util.IntList;
import xdev.util.MathUtils;
import xdev.util.ObjectUtils;
import xdev.util.StringUtils;
import xdev.util.logging.LoggerFactory;
import xdev.util.logging.XdevLogger;
import xdev.vt.VirtualTable;
import xdev.vt.VirtualTableColumn;


/**
 *
 * The {@link ItemList} is the backend for some {@link ListModel}s in XDEV.
 *
 * 

* The {@link ItemList} consists of entries ({@link Entry}) which provide two * stores: item and data. The item is * used to display the list entry in a GUI component. The data * object is not displayed and used as a hidden store for data like ids * or references. *

* * @see XdevListBox * @see XdevComboBox * * @see XdevListBox#setItemList(ItemList) * @see XdevComboBox#setItemList(ItemList) * * @author XDEV Software * * @since 2.0 * */ public class ItemList implements Copyable { /** * Logger instance for this class. */ private static final XdevLogger log = LoggerFactory.getLogger(ItemList.class); /** * An entry in an {@link ItemList} holding *
    *
  • a data object
  • *
  • a display item
  • *
  • an icon
  • *
  • settings: enabled
  • *
  • and client properties
  • *
* * @author XDEV Software * */ public static class Entry implements Copyable { private Object item; private Object data; private Icon icon; private boolean enabled = true; private Map clientProperties; /** * Creates a new entry with the specified data object, the * display item is the String value of data. * * @param data * the data object */ public Entry(final Object data) { this(String.valueOf(data),data); } /** * Creates a new entry with the specified data object and * display item. * * @param item * the display item * @param data * the data object */ public Entry(final Object item, final Object data) { this(item,data,null); } /** * Creates a new entry with the specified data object, a * display item and an icon. * * @param item * the display item * @param data * the data object * @param icon * the display icon */ public Entry(final Object item, final Object data, final Icon icon) { this.item = item; this.data = data; this.icon = icon; } /** * Returns the diplay item of this entry. * * @return the display item */ public Object getItem() { return this.item; } /** * Sets a new display item for this entry. * * @param item * the new display item */ public void setItem(final Object item) { this.item = item; } /** * Returns the data object of this entry. * * @return the data object */ public Object getData() { return this.data; } /** * Sets a new data object for this entry. * * @param data * the new data object */ public void setData(final Object data) { this.data = data; } /** * Returns the optional icon of this entry. * * @return the icon (may be null) */ public Icon getIcon() { return this.icon; } /** * Sets the display icon of this entry. * * @param icon * the new diplay icon */ public void setIcon(final Icon icon) { this.icon = icon; } /** * Determines whether this entry is enabled. * * @return true if the entry is enabled, false * otherwise */ public boolean isEnabled() { return this.enabled; } /** * Enables or disables this entry, depending on the value of the * parameter enabled. *

* A disabled entry is renderered disabled and cannot be selected. * * @param enabled * If true, this entry is enabled; otherwise * this entry is disabled */ public void setEnabled(final boolean enabled) { this.enabled = enabled; } /** * Adds an arbitrary key/value "client property" to this entry. *

* The get/putClientProperty methods provide access to a * small per-instance hashtable. Callers can use get/putClientProperty * to annotate entries. *

* If value is null this method will remove the property. * * @param key * the new client property key * @param value * the new client property value; if null this * method will remove the property * @see #getClientProperty */ public void putClientProperty(final Object key, final Object value) { if(value == null && this.clientProperties == null) { return; } if(this.clientProperties == null) { this.clientProperties = new HashMap(); } if(value == null) { this.clientProperties.remove(key); } else { this.clientProperties.put(key,value); } } /** * Returns the value of the property with the specified key. Only * properties added with putClientProperty will return a * non-null value. * * @param key * the being queried * @return the value of this property or null * @see #putClientProperty */ public Object getClientProperty(final Object key) { if(this.clientProperties == null) { return null; } else { return this.clientProperties.get(key); } } /** * {@inheritDoc} */ @Override public String toString() { return String.valueOf(this.item); } /** * Compares obj with this entry. *

* Two entries are considered equal if their item and data objects are * equal. * * @param obj * the Object to compare * * @return true if equal, otherwise false * * @see #getData() */ @Override public boolean equals(final Object obj) { if(obj == this) { return true; } if(obj instanceof Entry) { final Entry other = (Entry)obj; return ObjectUtils.equals(this.item,other.item) && ObjectUtils.equals(this.data,other.data); } return false; } /** * {@inheritDoc} */ @Override public int hashCode() { return MathUtils.computeHash(this.item,this.data); } /** * {@inheritDoc} */ @Override public Entry clone() { final Entry entry = new Entry(this.item,this.data,this.icon); if(this.clientProperties != null) { entry.clientProperties = new HashMap(this.clientProperties); } entry.enabled = this.enabled; return entry; } } private final List listeners = new Vector(); private final List entries = new ArrayList(); private VirtualTable virtualTable; private String itemCol, dataCol; /** * @since 4.0 */ private ItemListPageControl pageControl; /** * Creates a new empty {@link ItemList}. */ public ItemList() { } /** * Creates a new {@link ItemList} based on the provided {@link List}s * item and data. Item and * data must be non null and have an equal size. The first * object in item is mapped to first object in * data ... and so on. * * @param item * {@link List} to use as items * @param data * {@link List} to use as data */ public ItemList(@NotNull final Collection item, @NotNull final Collection data) { if(item == null || data == null || item.size() != data.size()) { throw new IllegalArgumentException( "item and data must be non null and have an equal size"); } addAll(item,data); } /** * Creates a new {@link ItemList} based on the provided arrays * item and data. Item and * data must be non null and have an equal size. The first * object in item is mapped to first object in * data ... and so on. * * @param item * array to use as items * @param data * array to use as data */ public ItemList(@NotNull final Object[] item, @NotNull final Object[] data) { if(item == null || data == null || item.length != data.length) { throw new IllegalArgumentException( "item and data must be non null and have an equal size"); } final int c = item.length; for(int i = 0; i < c; i++) { this.entries.add(new Entry(String.valueOf(item[i]),data[i])); } } /** * Create a new {@link ItemList} based on the provided {@link ListModel}. * The data are the model's values and the item's are the model's values as * {@link String}. * * @param model * The data model */ public ItemList(@NotNull final ListModel model) { addAll(model); } public ItemList(@NotNull final Collection entries) { addAll(entries); } /** * Adds a {@link ListDataListener} to this {@link ItemList}. * * @param listener * the {@link ListDataListener} to add */ public void addListDataListener(final ListDataListener listener) { this.listeners.add(listener); } /** * Removes the {@link ListDataListener} from this {@link ItemList}. * * @param listener * the {@link ListDataListener} to remove */ public void removeListDataListener(final ListDataListener listener) { this.listeners.remove(listener); } protected void fireContentsChanged(final int index0, final int index1) { if(this.listeners.size() > 0) { final ListDataEvent event = new ListDataEvent(this,ListDataEvent.CONTENTS_CHANGED, index0,index1); for(final ListDataListener listener : this.listeners .toArray(new ListDataListener[this.listeners.size()])) { listener.contentsChanged(event); } } } protected void fireIntervalAdded(final int index0, final int index1) { if(this.listeners.size() > 0) { final ListDataEvent event = new ListDataEvent(this,ListDataEvent.INTERVAL_ADDED,index0, index1); for(final ListDataListener listener : this.listeners .toArray(new ListDataListener[this.listeners.size()])) { listener.intervalAdded(event); } } } protected void fireIntervalRemoved(final int index0, final int index1) { if(this.listeners.size() > 0) { final ListDataEvent event = new ListDataEvent(this,ListDataEvent.INTERVAL_REMOVED, index0,index1); for(final ListDataListener listener : this.listeners .toArray(new ListDataListener[this.listeners.size()])) { listener.intervalRemoved(event); } } } /** * Creates a (defensive) copy of this {@link ItemList}. * * @return a (defensive) copy of this {@link ItemList}. */ public ItemList copy() { return new ItemList(this.entries); } /** * {@inheritDoc} */ @Override public ItemList clone() { return this.copy(); } /** * Adds a new row / item data pair to the end of this {@link ItemList}. * * @param item * to add * @param data * to add */ public void add(final String item, final Object data) { add(size(),item,data); } /** * Adds a new row / item data pair at the specified index to * this {@link ItemList}. * * @param index * index at which the specified element is to be inserted * @param item * the display item to add * @param data * the data object to add */ public void add(final int index, final String item, final Object data) { add(index,new Entry(item,data)); } /** * Adds the entry to the end of this {@link ItemList}. * * @param entry * the entry to add */ public void add(final Entry entry) { add(size(),entry); } /** * Adds the entry at the specified index to this * {@link ItemList}. * * @param index * index at which the specified element is to be inserted * @param entry * the entry to add */ public void add(final int index, final Entry entry) { this.entries.add(index,entry); fireIntervalAdded(index,index); } /** * Adds all entries of item and data to this * {@link ItemList}. * * @param item * {@link Collection} of items to add * @param data * {@link Collection} of data to add */ public void addAll(final Collection item, final Collection data) { final int oldSize = this.entries.size(); final Object[] itemElems = item.toArray(); final Object[] dataElems = data.toArray(); final int max = Math.min(itemElems.length,dataElems.length); for(int i = 0; i < max; i++) { this.entries.add(new Entry(String.valueOf(itemElems[i]),dataElems[i])); } fireIntervalAdded(oldSize,this.entries.size() - 1); } /** * Adds all entries to this {@link ItemList}. * * @param entries * the entries to add */ public void addAll(final Collection entries) { final int oldSize = entries.size(); for(final Entry entry : entries) { this.entries.add(entry.clone()); } fireIntervalAdded(oldSize,entries.size() - 1); } /** * Adds all entries of given {@link ItemList} il to this * {@link ItemList}. * * @param il * {@link ItemList} to add */ public void addAll(final ItemList il) { addAll(il.entries); } /** * Adds all entries of the model to this {@link ItemList}. * * @param model * the data model */ public void addAll(final ListModel model) { final int oldSize = this.entries.size(); final int c = model.getSize(); for(int i = 0; i < c; i++) { this.entries.add(new Entry(model.getElementAt(i))); } fireIntervalAdded(oldSize,this.entries.size() - 1); } /** * Removes the element at the specified position in this {@link ItemList}. * Shifts any subsequent elements to the top (subtracts one from their * indices). Returns the element that was removed from the list. * * @param index * the index of the element to be removed * * @return the element previously at the specified position * * @throws IndexOutOfBoundsException * if the index is out of range ( * index < 0 || index >= size()) */ public Entry remove(final int index) throws IndexOutOfBoundsException { final Entry entry = this.entries.remove(index); fireIntervalRemoved(index,index); return entry; } /** * Removes the elements at the specified positions in this {@link ItemList}. * Shifts any subsequent elements to the top (subtracts one from their * indices). Returns the elements that were removed from the list. * * @param indices * the indices of the elements to be removed * * @return a array of element's previously at the specified position's * * @throws IndexOutOfBoundsException * if an index is out of range ( * index < 0 || index >= size()) */ public Entry[] removeAll(final int... indices) throws IndexOutOfBoundsException { final int[] sorted = ArrayUtils.copyOf(indices); Arrays.sort(sorted); final Entry[] entries = new Entry[sorted.length]; for(int i = sorted.length, ei = 0; --i >= 0; ei++) { entries[ei] = remove(sorted[i]); } return entries; } /** * Returns the entry at the specified position in this {@link ItemList}. * * @param index * index of the entry to return * @return the entry at the specified position in this list * @throws IndexOutOfBoundsException * if the index is out of range ( * index < 0 || index >= size()) */ public Entry get(final int index) throws IndexOutOfBoundsException { return this.entries.get(index); } /** * Returns the item at the specified position in this {@link ItemList}. * * @param index * index of the item to return * @return the element at the specified position in this list * @throws IndexOutOfBoundsException * if the index is out of range ( * index < 0 || index >= size()) */ public Object getItem(final int index) throws IndexOutOfBoundsException { return get(index).getItem(); } /** * Returns the data at the specified position in this {@link ItemList}. * * @param index * index of the data to return * @return the element at the specified position in this list * @throws IndexOutOfBoundsException * if the index is out of range ( * index < 0 || index >= size()) */ public Object getData(final int index) throws IndexOutOfBoundsException { return get(index).getData(); } /** * Returns the size of the list. * * @return the size of the list */ public int size() { return this.entries.size(); } /** * Removes all of the elements from this {@link ItemList}. The * {@link ItemList} will be empty after this call returns. * */ public void clear() { final int size = size(); if(size > 0) { this.entries.clear(); fireIntervalRemoved(0,size - 1); } } /** * @since 4.0 */ void setPageControl(final ItemListPageControl pageControl) { this.pageControl = pageControl; } /** * public @since 4.0 */ public void syncWithVT() { setModelImpl(this.virtualTable,this.itemCol,this.dataCol,null); } /*** * Adds all row data (formatted) of the given {@link VirtualTable} * vt to this {@link ItemList}. * * @param vt * {@link VirtualTable} to read the rows from. * @param itemCol * columnname to fill item from or string with * variables like "{%SURNAME} {%NAME} - {%AGE}" * @param dataCol * column name to fill data from, or multiple * columns names, comma-separated */ public void setModel(final VirtualTable vt, final String itemCol, final String dataCol) { setModel(vt,itemCol,dataCol,false); } /*** * Adds all row data (formatted) of the given {@link VirtualTable} * vt to this {@link ItemList}. * * @param vt * {@link VirtualTable} to read the rows from. * @param itemCol * columnname to fill item from or string with * variables like "{%SURNAME} {%NAME} - {%AGE}" * @param dataCol * column name to fill data from, or multiple * columns names, comma-separated * @param queryData * if {@code true}, the best fitting select for this {@code vt} * is used */ public void setModel(final VirtualTable vt, final String itemCol, final String dataCol, final boolean queryData) { setModel(vt,itemCol,dataCol,queryData,false); } /*** * Adds all row data (formatted) of the given {@link VirtualTable} * vt to this {@link ItemList}. * * @param vt * {@link VirtualTable} to read the rows from. * @param itemCol * columnname to fill item from or string with * variables like "{%SURNAME} {%NAME} - {%AGE}" * @param dataCol * column name to fill data from, or multiple * columns names, comma-separated * @param queryData * if {@code true}, the best fitting select for this {@code vt} * is used * @param selectiveQuery * if {@code true}, only the used columns are * queried * @since 5.0 */ public void setModel(final VirtualTable vt, final String itemCol, final String dataCol, final boolean queryData, final boolean selectiveQuery) { SELECT select = null; if(queryData) { select = getDefaultModelQuery(vt,itemCol,dataCol,selectiveQuery); } setModel(vt,itemCol,dataCol,select); } /*** * Adds all row data (formatted) of the given {@link VirtualTable} * vt to this {@link ItemList}. * * @param vt * {@link VirtualTable} to read the rows from. * @param itemCol * columnname to fill item from or string with * variables like "{%SURNAME} {%NAME} - {%AGE}" * @param dataCol * column name to fill data from, or multiple * columns names, comma-separated * @param select * a custom {@link SELECT} for filtering or ordering the results * of the {@code vt}, may be null for the default select * @param params * a set of parameters related to the param {@code select}, may * be null */ public void setModel(final VirtualTable vt, final String itemCol, final String dataCol, final SELECT select, final Object... params) { setModelImpl(vt.clone(true),itemCol,dataCol,select,params); } private void setModelImpl(final VirtualTable vt, final String itemCol, final String dataCol, final SELECT select, final Object... params) { this.virtualTable = vt; this.itemCol = itemCol; this.dataCol = dataCol; if(select != null) { try { if(this.pageControl != null) { this.pageControl.changeModel(select,params,0); } else { vt.queryAndFill(select,params); } } catch(final Exception e) { log.error(e); } } clear(); if(vt.getRowCount() == 0) { return; } final int itemColumnIndex = vt.getColumnIndex(itemCol); final String[] dataColNames = FormularComponentSupport.getDataFields(dataCol); final IntList dataColIndexes = new IntList(); for(final String dataColName : dataColNames) { final int index = vt.getColumnIndex(dataColName); if(index != -1) { dataColIndexes.add(index); } } final int dataColIndexCount = dataColIndexes.size(); final int c = vt.getRowCount(); for(int row = 0; row < c; row++) { String item; if(itemColumnIndex != -1) { item = vt.getFormattedValueAt(row,itemColumnIndex); } else { item = vt.format(row,itemCol); } Object data; if(dataColIndexCount == 1) { data = vt.getValueAt(row,dataColIndexes.get(0)); } else if(dataColIndexCount > 1) { final Object[] array = new Object[dataColIndexCount]; for(int i = 0; i < dataColIndexCount; i++) { array[i] = vt.getValueAt(row,dataColIndexes.get(i)); } data = array; } else { data = null; } this.entries.add(new Entry(item,data)); } fireIntervalAdded(0,size() - 1); } /** * Reloads the data from the underlying data source with the last executed * resp. default query. */ public void refresh() { if(this.virtualTable != null && this.itemCol != null) { try { if(this.pageControl != null) { this.pageControl.refresh(); } else if(this.virtualTable.getLastQuery() != null) { this.virtualTable.reload(); } else { this.virtualTable.queryAndFill(); } syncWithVT(); } catch(final Exception e) { log.error(e); } } } /** * Reloads the data form the underlying data source. *

* The last executed query is extended with condition. * * @param condition * The additional filter for the query * @param params * param objects used in condition */ public void updateModel(final Condition condition, Object... params) { if(this.virtualTable != null && this.itemCol != null) { final QueryInfo lastQuery = this.virtualTable.getLastQuery(); final QueryInfo queryClone = lastQuery != null ? lastQuery.clone() : null; SELECT select; if(lastQuery != null) { select = lastQuery.getSelect(); final Object[] lastParams = lastQuery.getParameters(); if(lastParams.length > 0) { params = ArrayUtils.concat(Object.class,lastParams,params); } } else { select = getDefaultModelQuery(this.virtualTable,this.itemCol,this.dataCol,false); } if(condition != null) { WHERE where = select.getWhere(); if(where != null && !where.isEmpty()) { where = new WHERE(where.encloseWithPars().and(condition)); } else { where = new WHERE(condition); } select.WHERE(where); } setModel(this.virtualTable,this.itemCol,this.dataCol,select,params); this.virtualTable.setLastQuery(queryClone); } } static SELECT getDefaultModelQuery(final VirtualTable vt, final String itemCol, final String dataCol, final boolean selectiveQuery) { final SELECT select; if(selectiveQuery) { final Set columnNames = new LinkedHashSet<>(); // always query primary key columns for(final VirtualTableColumn pkColumn : vt.getPrimaryKeyColumns()) { columnNames.add(pkColumn.getName()); } if(vt.getColumn(itemCol) != null) { columnNames.add(itemCol); } else { StringUtils.format(itemCol,key -> { columnNames.add(key); return key; }); } if(dataCol != null && dataCol.length() > 0) { for(final String name : FormularComponentSupport.getDataFields(dataCol)) { columnNames.add(name); } } select = vt .getSelect(vt.getColumns(columnNames.toArray(new String[columnNames.size()]))); } else { select = vt.getSelect(); } int itemColumnIndex = vt.getColumnIndex(itemCol); if(itemColumnIndex == -1) { int start; int searchStart = 0; while(itemColumnIndex == -1 && (start = itemCol.indexOf("{%",searchStart)) >= 0) { final int end = itemCol.indexOf("}",start + 2); if(end > start) { final String col = itemCol.substring(start + 2,end); itemColumnIndex = vt.getColumnIndex(col); searchStart = end; } } } if(itemColumnIndex != -1) { select.ORDER_BY(vt.getColumnAt(itemColumnIndex)); } return select; } /** * Returns the {@link VirtualTable} which was associated via * {@link #setModel(VirtualTable, String, String)}, or null if * no {@link VirtualTable} has been associated yet. * * * @return The {@link VirtualTable} which was associated via * {@link #setModel(VirtualTable, String, String)}, or * null */ public VirtualTable getVirtualTable() { return this.virtualTable; } /** * Returns true if this {@link ItemList} contains the specified * item. More formally, returns true if and only if * this list contains at least one element e such that * (o==null ? e==null : o.equals(e)). * * @param item * item whose presence in this list is to be tested * @return true if this list contains the specified element */ public boolean containsItem(final Object item) { return indexOfItem(item) != -1; } /** * Returns true if this {@link ItemList} contains the specified * entry. More formally, returns true if and only if * this list contains at least one element e such that * (o==null ? e==null : o.equals(e)). * * @param entry * item whose presence in this list is to be tested * @return true if this list contains the specified element */ public boolean contains(final Entry entry) { return this.entries.contains(entry); } /** * Returns the index of the first occurrence of the specified * item in this list, or -1 if this {@link ItemList} does not * contain the item. More formally, returns the lowest index * i such that * (o==null ? get(i)==null : o.equals(get(i))), * or -1 if there is no such index. * * @param item * item to search for * @return the index of the first occurrence of the specified * item in this list, or -1 if this list does not * contain the item */ public int indexOfItem(final Object item) { final int c = size(); for(int i = 0; i < c; i++) { if(ObjectUtils.equals(item,get(i).getItem())) { return i; } } return -1; } /** * Returns true if this {@link ItemList} contains the specified * data. More formally, returns true if and only if * this list contains at least one element e such that * (o==null ? e==null : o.equals(e)). * * @param data * data whose presence in this list is to be tested * @return true if this list contains the specified element */ public boolean containsData(final Object data) { return indexOfData(data) != -1; } /** * Returns the index of the first occurrence of the specified * data in this list, or -1 if this {@link ItemList} does not * contain the data. More formally, returns the lowest index * i such that * (o==null ? get(i)==null : o.equals(get(i))), * or -1 if there is no such index. * * @param data * data to search for * @return the index of the first occurrence of the specified * item in this list, or -1 if this list does not * contain the data */ public int indexOfData(final Object data) { final int c = size(); for(int i = 0; i < c; i++) { if(ObjectUtils.equals(data,get(i).getData())) { return i; } } return -1; } /** * Returns a {@link List} containing all items of this * {@link ItemList}. * * @return a {@link List} containing all items of this * {@link ItemList}. */ public List getItemsAsList() { final int c = size(); final List items = new ArrayList(c); for(int i = 0; i < c; i++) { items.add(getItem(i)); } return items; } /** * Returns a {@link List} containing all datas of this * {@link ItemList}. * * @return a {@link List} containing all datas of this * {@link ItemList}. */ public List getDataAsList() { final int c = size(); final List data = new ArrayList(c); for(int i = 0; i < c; i++) { data.add(getData(i)); } return data; } /** * Sets the size of this {@link ItemList}. If the {@link ItemList} has more * entries that the specified size, list entries will be * removed beginning with the largest index. If the {@link ItemList} has * less than the specified size, list entries (item = "", data * = "") will be added at the end of this {@link ItemList}. * * @param size * new size of the {@link ItemList} */ public void setSize(final int size) { final int thisSize = this.entries.size(); if(thisSize < size) { while(this.entries.size() < size) { this.entries.add(new Entry("","")); } fireIntervalAdded(thisSize,size() - 1); } else if(thisSize > size) { while(this.entries.size() > size) { this.entries.remove(this.entries.size() - 1); } fireIntervalRemoved(size - 1,thisSize - 1); } } /** * {@inheritDoc} */ @Override public String toString() { return StringUtils.concat(", ",this.entries); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy