Java tutorial
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved. * * The contents of this file are subject to the terms of the GNU General Public License Version 3 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each file. */ package org.openconcerto.sql.element; import static org.openconcerto.sql.TM.getTM; import org.openconcerto.sql.Configuration; import org.openconcerto.sql.Log; import org.openconcerto.sql.TM; import org.openconcerto.sql.model.DBStructureItemNotFound; import org.openconcerto.sql.model.SQLField; import org.openconcerto.sql.model.SQLFieldsSet; import org.openconcerto.sql.model.SQLRow; import org.openconcerto.sql.model.SQLRowAccessor; import org.openconcerto.sql.model.SQLRowMode; import org.openconcerto.sql.model.SQLRowValues; import org.openconcerto.sql.model.SQLSelect; import org.openconcerto.sql.model.SQLSelect.ArchiveMode; import org.openconcerto.sql.model.SQLTable; import org.openconcerto.sql.model.Where; import org.openconcerto.sql.model.graph.DatabaseGraph; import org.openconcerto.sql.model.graph.Link; import org.openconcerto.sql.request.ComboSQLRequest; import org.openconcerto.sql.request.ListSQLRequest; import org.openconcerto.sql.request.SQLCache; import org.openconcerto.sql.request.SQLFieldTranslator; import org.openconcerto.sql.sqlobject.SQLTextCombo; import org.openconcerto.sql.users.rights.UserRightsManager; import org.openconcerto.sql.utils.SQLUtils; import org.openconcerto.sql.utils.SQLUtils.SQLFactory; import org.openconcerto.sql.view.list.IListeAction; import org.openconcerto.sql.view.list.SQLTableModelColumn; import org.openconcerto.sql.view.list.SQLTableModelColumnPath; import org.openconcerto.sql.view.list.SQLTableModelSourceOnline; import org.openconcerto.utils.CollectionMap; import org.openconcerto.utils.CollectionUtils; import org.openconcerto.utils.CompareUtils; import org.openconcerto.utils.ExceptionHandler; import org.openconcerto.utils.ExceptionUtils; import org.openconcerto.utils.Tuple2; import org.openconcerto.utils.cache.CacheResult; import org.openconcerto.utils.cc.IClosure; import org.openconcerto.utils.cc.ITransformer; import org.openconcerto.utils.change.ListChangeIndex; import org.openconcerto.utils.change.ListChangeRecorder; import org.openconcerto.utils.i18n.Grammar; import org.openconcerto.utils.i18n.Grammar_fr; import org.openconcerto.utils.i18n.NounClass; import org.openconcerto.utils.i18n.Phrase; import java.awt.Component; import java.lang.reflect.Constructor; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.SortedMap; import java.util.logging.Level; import javax.swing.JComponent; import javax.swing.JOptionPane; import javax.swing.text.JTextComponent; import net.jcip.annotations.GuardedBy; import org.apache.commons.collections.MapIterator; import org.apache.commons.collections.MultiMap; import org.apache.commons.collections.iterators.EntrySetMapIterator; /** * Dcrit comment manipuler un lment de la BD (pas forcment une seule table, voir * privateForeignField). * * @author ilm */ public abstract class SQLElement { static final private Set<String> computingFF = Collections.unmodifiableSet(new HashSet<String>()); static final private Set<SQLField> computingRF = Collections.unmodifiableSet(new HashSet<SQLField>()); private static Phrase createPhrase(String singular, String plural) { final NounClass nounClass; final String base; if (singular.startsWith("une ")) { nounClass = NounClass.FEMININE; base = singular.substring(4); } else if (singular.startsWith("un ")) { nounClass = NounClass.MASCULINE; base = singular.substring(3); } else { nounClass = null; base = singular; } final Phrase res = new Phrase(Grammar_fr.getInstance(), base, nounClass); if (nounClass != null) res.putVariant(Grammar.INDEFINITE_ARTICLE_SINGULAR, singular); res.putVariant(Grammar.PLURAL, plural); return res; } // from the most loss of information to the least. public static enum ReferenceAction { /** If a referenced row is archived, empty the foreign field */ SET_EMPTY, /** If a referenced row is archived, archive this row too */ CASCADE, /** If a referenced row is to be archived, abort the operation */ RESTRICT } static final public String DEFAULT_COMP_ID = "default component code"; /** * If this value is passed to the constructor, {@link #createCode()} will only be called the * first time {@link #getCode()} is. This allow the method to use objects passed to the * constructor of a subclass. */ static final public String DEFERRED_CODE = new String("deferred code"); @GuardedBy("this") private SQLElementDirectory directory; private String l18nPkgName; private Class<?> l18nClass; private Phrase name; private final SQLTable primaryTable; // used as a key in SQLElementDirectory so it should be immutable private String code; private ComboSQLRequest combo; private ListSQLRequest list; private SQLTableModelSourceOnline tableSrc; private final ListChangeRecorder<IListeAction> rowActions; private final CollectionMap<String, ITransformer<Tuple2<SQLElement, String>, SQLComponent>> components; // foreign fields private Set<String> normalFF; private String parentFF; private Set<String> sharedFF; private Map<String, SQLElement> privateFF; private final Map<String, ReferenceAction> actions; // referent fields private Set<SQLField> childRF; private Set<SQLField> privateParentRF; private Set<SQLField> otherRF; // lazy creation private SQLCache<SQLRowAccessor, Object> modelCache; private final Map<String, JComponent> additionalFields; private final List<SQLTableModelColumn> additionalListCols; @GuardedBy("this") private List<String> mdPath; @Deprecated public SQLElement(String singular, String plural, SQLTable primaryTable) { this(primaryTable, createPhrase(singular, plural)); } public SQLElement(SQLTable primaryTable) { this(primaryTable, null); } public SQLElement(final SQLTable primaryTable, final Phrase name) { this(primaryTable, name, null); } public SQLElement(final SQLTable primaryTable, final Phrase name, final String code) { super(); if (primaryTable == null) { throw new DBStructureItemNotFound("table is null for " + this.getClass()); } this.primaryTable = primaryTable; this.setL18nPackageName(null); this.setDefaultName(name); this.code = code == null ? createCode() : code; this.combo = null; this.list = null; this.rowActions = new ListChangeRecorder<IListeAction>(new ArrayList<IListeAction>()); this.actions = new HashMap<String, ReferenceAction>(); this.resetRelationships(); this.components = new CollectionMap<String, ITransformer<Tuple2<SQLElement, String>, SQLComponent>>( new LinkedList<ITransformer<Tuple2<SQLElement, String>, SQLComponent>>()); this.modelCache = null; // the components should always be in the same order this.additionalFields = new LinkedHashMap<String, JComponent>(); this.additionalListCols = new ArrayList<SQLTableModelColumn>(); this.mdPath = Collections.emptyList(); } /** * Should return the code for this element. This method is only called if the <code>code</code> * parameter of the constructor is <code>null</code>. * * @return the default code for this element. */ protected String createCode() { return getClass().getName() + "-" + getTable().getName(); } /** * Must be called if foreign/referent keys are added or removed. */ public synchronized void resetRelationships() { this.privateFF = null; this.parentFF = null; this.normalFF = null; this.sharedFF = null; this.actions.clear(); this.childRF = null; this.privateParentRF = null; this.otherRF = null; } protected synchronized final boolean areRelationshipsInited() { return this.sharedFF != null; } private void checkSelfCall(boolean check, final String methodName) { assert check : this + " " + methodName + "() is calling itself, and thus the caller will only see a partial state"; } private synchronized void initFF() { checkSelfCall(this.sharedFF != computingFF, "initFF"); if (areRelationshipsInited()) return; this.sharedFF = computingFF; final Set<String> privates = new HashSet<String>(this.getPrivateFields()); this.privateFF = new HashMap<String, SQLElement>(privates.size()); final Set<String> parents = new HashSet<String>(); this.normalFF = new HashSet<String>(); final Set<String> tmpSharedFF = new HashSet<String>(); for (final SQLField ff : this.getTable().getForeignKeys()) { final String fieldName = ff.getName(); final SQLElement foreignElement = this.getForeignElement(fieldName); if (privates.contains(fieldName)) { privates.remove(fieldName); this.privateFF.put(fieldName, foreignElement); } else if (foreignElement.isShared()) { tmpSharedFF.add(fieldName); } else if (foreignElement.getChildrenReferentFields().contains(ff)) { parents.add(fieldName); } else { this.normalFF.add(fieldName); } } if (parents.size() > 1) throw new IllegalStateException("for " + this + " more than one parent :" + parents); this.parentFF = parents.size() == 0 ? null : (String) parents.iterator().next(); if (privates.size() > 0) throw new IllegalStateException( "for " + this + " these private foreign fields are not valid :" + privates); this.sharedFF = tmpSharedFF; // MAYBE move required fields to SQLElement and use RESTRICT this.actions.put(this.parentFF, ReferenceAction.CASCADE); for (final String s : this.privateFF.keySet()) { this.actions.put(s, ReferenceAction.SET_EMPTY); } for (final String s : this.normalFF) { this.actions.put(s, ReferenceAction.SET_EMPTY); } for (final String s : this.sharedFF) { this.actions.put(s, ReferenceAction.RESTRICT); } this.ffInited(); } protected void ffInited() { // MAYBE use DELETE_RULE of Link } // convert the list of String of getChildren() to a Set of SQLField pointing to this table private synchronized Set<SQLField> computeChildrenRF() { final Set<SQLField> res = new HashSet<SQLField>(); // eg "BATIMENT" or "BATIMENT.ID_SITE" for (final String child : this.getChildren()) { // a field from our child to us, eg |BATIMENT.ID_SITE| final SQLField childField; final int comma = child.indexOf(','); final String tableName = comma < 0 ? child : child.substring(0, comma); final SQLTable childTable = this.getTable().getTable(tableName); if (comma < 0) { final Set<SQLField> keys = childTable.getForeignKeys(this.getTable()); if (keys.size() != 1) throw new IllegalArgumentException( "cannot find a foreign from " + child + " to " + this.getTable()); childField = keys.iterator().next(); } else { childField = childTable.getField(child.substring(comma + 1)); final SQLTable foreignTable = childField.getDBSystemRoot().getGraph().getForeignTable(childField); if (!foreignTable.equals(this.getTable())) { throw new IllegalArgumentException(childField + " doesn't point to " + this.getTable()); } } res.add(childField); } return res; } private synchronized void initRF() { checkSelfCall(this.otherRF != computingRF, "initRF"); if (this.otherRF != null) return; this.otherRF = computingRF; this.privateParentRF = new HashSet<SQLField>(); final Set<SQLField> tmpOtherRF = new HashSet<SQLField>(); for (final SQLField refField : this.getTable().getBase().getGraph().getReferentKeys(this.getTable())) { // don't force every table to have an SQLElement (eg ELEMENT_MISSION) final SQLElement refElem = this.getElementLenient(refField.getTable()); if (refElem != null && refElem.getPrivateForeignFields().contains(refField.getName())) { this.privateParentRF.add(refField); } else if (!this.getChildrenReferentFields().contains(refField)) { tmpOtherRF.add(refField); } } this.otherRF = tmpOtherRF; } // childRF is done outside initRF() to avoid : // MISSION.initRF() -> ELEMENT_MISSION.getPrivateForeignFields() -> // ELEMENT_MISSION.initFF() -> MISSION.getChildrenReferentFields() -> MISSION.initRF() private synchronized void initChildRF() { checkSelfCall(this.childRF != computingRF, "initFF"); if (this.childRF != null) return; this.childRF = computingRF; final Set<SQLField> children = this.computeChildrenRF(); final Set<SQLField> tmpChildRF = new HashSet<SQLField>(); for (final SQLField refField : this.getTable().getBase().getGraph().getReferentKeys(this.getTable())) { // don't force every table to have an SQLElement (eg ELEMENT_MISSION) final SQLElement refElem = this.getElementLenient(refField.getTable()); // if no element found, treat as elements with no parent final SQLField refParentFF = refElem == null ? null : refElem.getParentFF(); // check coherence, either overload getParentFFName() or use getChildren(), but not both if (refParentFF != null && children.contains(refField)) throw new IllegalStateException(refElem + " specifies this as its parent: " + refParentFF + " and is also mentioned as our (" + this + ") child: " + refField); if (children.contains(refField) || refParentFF == refField) { tmpChildRF.add(refField); } } // pas besoin de faire comme dans initFF pour vrifier children : // computeChildrenRF le fait dj this.childRF = tmpChildRF; } final void setDirectory(final SQLElementDirectory directory) { // since this method should only be called at the end of SQLElementDirectory.addSQLElement() assert directory == null || directory.getElement(this.getTable()) == this; synchronized (this) { if (this.directory != directory) { if (this.areRelationshipsInited()) this.resetRelationships(); this.directory = directory; } } } public synchronized final SQLElementDirectory getDirectory() { return this.directory; } final SQLElement getElement(SQLTable table) { final SQLElement res = getElementLenient(table); if (res == null) throw new IllegalStateException("no element for " + table.getSQLName()); return res; } final SQLElement getElementLenient(SQLTable table) { synchronized (this) { return this.getDirectory().getElement(table); } } public final SQLElement getForeignElement(String foreignField) { try { return this.getElement(this.getForeignTable(foreignField)); } catch (RuntimeException e) { throw new IllegalStateException("no element for " + foreignField + " in " + this, e); } } private final SQLTable getForeignTable(String foreignField) { return this.getTable().getBase().getGraph().getForeignTable(this.getTable().getField(foreignField)); } public final synchronized String getL18nPackageName() { return this.l18nPkgName; } public final synchronized Class<?> getL18nClass() { return this.l18nClass; } public final void setL18nLocation(Class<?> clazz) { this.setL18nLocation(clazz.getPackage().getName(), clazz); } public final void setL18nPackageName(String name) { this.setL18nLocation(name, null); } /** * Set the location for the localized name. * * @param name a package name, can be <code>null</code> : * {@link SQLElementDirectory#getL18nPackageName()} will be used. * @param ctxt the class loader to load the resource, <code>null</code> meaning this class. * @see SQLElementDirectory#getName(SQLElement) */ public final synchronized void setL18nLocation(final String name, final Class<?> ctxt) { this.l18nPkgName = name; this.l18nClass = ctxt == null ? this.getClass() : ctxt; } /** * Set the default name, used if no translations could be found. * * @param name the default name, if <code>null</code> the {@link #getTable() table} name will be * used. */ public final synchronized void setDefaultName(Phrase name) { this.name = name != null ? name : Phrase.getInvariant(getTable().getName()); } /** * The default name. * * @return the default name, never <code>null</code>. */ public final synchronized Phrase getDefaultName() { return this.name; } /** * The name of this element in the current locale. * * @return the name of this, {@link #getDefaultName()} if there's no {@link #getDirectory() * directory} or if it hasn't a name for this. * @see SQLElementDirectory#getName(SQLElement) */ public final Phrase getName() { final SQLElementDirectory dir = this.getDirectory(); final Phrase res = dir == null ? null : dir.getName(this); return res == null ? this.getDefaultName() : res; } public String getPluralName() { return this.getName().getVariant(Grammar.PLURAL); } public String getSingularName() { return this.getName().getVariant(Grammar.INDEFINITE_ARTICLE_SINGULAR); } public CollectionMap<String, String> getShowAs() { // nothing by default return null; } /** * Fields that can neither be inserted nor updated. * * @return fields that cannot be modified. */ public Set<String> getReadOnlyFields() { return Collections.emptySet(); } /** * Fields that can only be set on insertion. * * @return fields that cannot be modified. */ public Set<String> getInsertOnlyFields() { return Collections.emptySet(); } private final SQLCache<SQLRowAccessor, Object> getModelCache() { if (this.modelCache == null) this.modelCache = new SQLCache<SQLRowAccessor, Object>(60, -1, "modelObjects of " + this.getCode()); return this.modelCache; } public void unarchiveNonRec(int id) throws SQLException { this.unarchiveNonRec(this.getTable().getRow(id)); } private void unarchiveNonRec(SQLRow row) throws SQLException { checkUndefined(row); if (!row.isArchived()) return; final Set<SQLRow> connectedRows = this.getArchivedConnectedRows(Collections.singleton(row)); for (final SQLRow desc : connectedRows) { getElement(desc.getTable()).unarchiveSingle(desc); } for (final SQLRow desc : connectedRows) { DeletionMode.UnArchiveMode.fireChange(desc); } } // *** getConnected* private Set<SQLRow> getArchivedConnectedRows(Collection<SQLRow> rows) throws SQLException { final Set<SQLRow> res = new HashSet<SQLRow>(); for (final SQLRow row : rows) { this.getElement(row.getTable()).getArchivedConnectedRows(row, res); } return res; } private void getArchivedConnectedRows(SQLRow row, Set<SQLRow> rows) throws SQLException { check(row); // si on tait dj dedans, ne pas continuer if (!rows.add(row)) return; // we want ARCHIVED existant and defined rows (since we never touch undefined ones) final SQLRowMode mode = new SQLRowMode(ArchiveMode.ARCHIVED, true, true); final Set<SQLRow> foreigns = new HashSet<SQLRow>(this.getNormalForeigns(row, mode).values()); final SQLRow parent = this.getForeignParent(row, mode); if (parent != null) { foreigns.add(parent); } // private ff are handled by DeletionMode // shared ff are never touched by DeletionMode // recurse for (final SQLRow foreign : foreigns) { this.getElement(foreign.getTable()).getArchivedConnectedRows(foreign, rows); } } // *** update /** * Compute the necessary steps to transform <code>from</code> into <code>to</code>. * * @param from the row currently in the db. * @param to the new values. * @return the script transforming <code>from</code> into <code>to</code>. */ public final UpdateScript update(SQLRowValues from, SQLRowValues to) { check(from); check(to); if (!from.hasID()) throw new IllegalArgumentException("missing id in " + from); if (from.getID() != to.getID()) throw new IllegalArgumentException("not the same row: " + from + " != " + to); final Set<SQLField> fks = this.getTable().getForeignKeys(); final UpdateScript res = new UpdateScript(this.getTable()); for (final String field : to.getFields()) { if (!fks.contains(this.getTable().getField(field))) { res.getUpdateRow().put(field, to.getObject(field)); } else { final Object fromPrivate = from.getObject(field); final Object toPrivate = to.getObject(field); if (!fromPrivate.getClass().equals(toPrivate.getClass())) throw new IllegalArgumentException("asymmetric tree " + fromPrivate + " != " + toPrivate); final boolean isPrivate = this.getPrivateForeignFields().contains(field); if (fromPrivate instanceof SQLRowValues) { final SQLRowValues fromPR = (SQLRowValues) fromPrivate; final SQLRowValues toPR = (SQLRowValues) toPrivate; if (isPrivate) { if (from.isForeignEmpty(field) && to.isForeignEmpty(field)) { // nothing to do, don't add to v } else if (from.isForeignEmpty(field)) { // insert, eg CPI.ID_OBS=1 -> CPI.ID_OBS={DES="rouill"} // clear referents otherwise we will merge the updateRow with the to // graph (toPR being a private is pointed to by its owner, which itself // points to others, but we just want the private) res.getUpdateRow().put(field, toPR.deepCopy().clearReferents()); } else if (to.isForeignEmpty(field)) { // archive res.addToArchive(this.getForeignElement(field), fromPR); } else { // neither is empty if (fromPR.getID() != toPR.getID()) throw new IllegalArgumentException( "private have changed: " + fromPR + " != " + toPR); res.put(field, this.getForeignElement(field).update(fromPR, toPR)); } } else { res.getUpdateRow().put(field, toPR.getID()); } } else { final Number fromP_ID = (Number) fromPrivate; final Number toP_ID = (Number) toPrivate; if (isPrivate) { // avoid Integer(3) != Long(3) if (fromP_ID.longValue() != toP_ID.longValue()) throw new IllegalArgumentException("cannot change private ID"); // if they're the same, nothing to do } else { res.getUpdateRow().put(field, toP_ID); } } } } return res; } public final void unarchive(int id) throws SQLException { this.unarchive(this.getTable().getRow(id)); } public void unarchive(final SQLRow row) throws SQLException { checkUndefined(row); // don't test row.isArchived() (it is done by getTree()) // to allow an unarchived parent to unarchive all its descendants. // nos descendants final List<SQLRow> descsAndMe = this.getTree(row, true); final Set<SQLRow> connectedRows = this.getArchivedConnectedRows(descsAndMe); SQLUtils.executeAtomic(this.getTable().getBase().getDataSource(), new SQLFactory<Object>() { @Override public Object create() throws SQLException { for (final SQLRow desc : connectedRows) { getElement(desc.getTable()).unarchiveSingle(desc); } for (final SQLRow desc : connectedRows) { DeletionMode.UnArchiveMode.fireChange(desc); } // reference // nothing to do : nobody points to an archived row return null; } }); } public final void archive(int id) throws SQLException { this.archive(this.getTable().getRow(id)); } public final void archive(SQLRow row) throws SQLException { this.archive(row, true); } /** * Archive la ligne demande et tous ses descendants mais ne cherche pas couper les rfrences * pointant sur ceux-ci. ATTN peut donc laisser la base dans un tat inconsistent, n'utiliser * que si aucun lien ne pointe sur ceux ci. En revanche, acclre grandement (par exemple pour * OBSERVATION) car pas besoin de chercher toutes les rfrences. * * @param id la ligne voulue. * @throws SQLException if pb while archiving. */ public final void archiveNoCut(int id) throws SQLException { this.archive(this.getTable().getRow(id), false); } protected void archive(final SQLRow row, final boolean cutLinks) throws SQLException { this.archive(new TreesOfSQLRows(this, row), cutLinks); } protected void archive(final TreesOfSQLRows trees, final boolean cutLinks) throws SQLException { if (trees.getElem() != this) throw new IllegalArgumentException(this + " != " + trees.getElem()); for (final SQLRow row : trees.getRows()) checkUndefined(row); SQLUtils.executeAtomic(this.getTable().getBase().getDataSource(), new SQLFactory<Object>() { @Override public Object create() throws SQLException { // reference // d'abord couper les liens qui pointent sur les futurs archivs if (cutLinks) { // TODO prend bcp de temps // FIXME update tableau pour chaque observation, ecrase les changements // faire : 'La base change voulez vous recharger ou garder vos modifs ?' final MultiMap externReferences = trees.getExternReferences(); // avoid toString() which might make requests to display rows (eg archived) if (Log.get().isLoggable(Level.FINEST)) Log.get().finest("will cut : " + externReferences); final MapIterator refIter = new EntrySetMapIterator(externReferences); while (refIter.hasNext()) { final SQLField refKey = (SQLField) refIter.next(); final Collection<?> refList = (Collection<?>) refIter.getValue(); final Iterator<?> listIter = refList.iterator(); while (listIter.hasNext()) { final SQLRow ref = (SQLRow) listIter.next(); ref.createEmptyUpdateRow().putEmptyLink(refKey.getName()).update(); } } Log.get().finest("done cutting links"); } // on archive tous nos descendants for (final SQLRowAccessor desc : trees.getFlatDescendants()) { getElement(desc.getTable()).archiveSingle(desc); // ne pas faire les fire aprs sinon qd on efface plusieurs lments de la mme // table : // on fire pour le 1er => updateSearchList => IListe.select(userID) // hors si userID a aussi t archiv (mais il n'y a pas eu son fire // correspondant), le component va lancer un RowNotFound DeletionMode.ArchiveMode.fireChange(desc); } // foreign field // nothing to do return null; } }); } private final void archiveSingle(SQLRowAccessor r) throws SQLException { this.changeSingle(r, DeletionMode.ArchiveMode); } private final void unarchiveSingle(SQLRowAccessor r) throws SQLException { this.changeSingle(r, DeletionMode.UnArchiveMode); } private final void changeSingle(SQLRowAccessor r, DeletionMode m) throws SQLException { m.execute(this, r); } public void delete(SQLRowAccessor r) throws SQLException { this.check(r); if (true) throw new UnsupportedOperationException("not yet implemented."); this.changeSingle(r, DeletionMode.DeleteMode); } public final SQLTable getTable() { return this.primaryTable; } /** * A code identifying a specific meaning for the table and fields. I.e. it is used by * {@link #getName() names} and {@link SQLFieldTranslator item metadata}. E.g. if two * applications use the same table for different purposes (at different times, of course), their * elements should not share a code. On the contrary, if one application merely adds a field to * an existing table, the new element should keep the same code so that existing name and * documentation remain. * * @return a code for the table and its meaning. */ public synchronized final String getCode() { if (this.code == DEFERRED_CODE) { final String createCode = this.createCode(); if (createCode == DEFERRED_CODE) throw new IllegalStateException("createCode() returned DEFERRED_CODE"); this.code = createCode; } return this.code; } /** * Is the rows of this element shared, ie rows are unique and must not be copied. * * @return <code>true</code> if this element is shared. */ public boolean isShared() { return false; } /** * Must the rows of this element be copied when traversing a hierarchy. * * @return <code>true</code> if the element must not be copied. */ public boolean dontDeepCopy() { return false; } // *** rf public final synchronized Set<SQLField> getOtherReferentFields() { this.initRF(); return this.otherRF; } public final synchronized Set<SQLField> getChildrenReferentFields() { this.initChildRF(); return this.childRF; } /** * The private foreign fields pointing to this table. Eg if this is OBSERVATION, * {SOURCE.ID_OBS1, SOURCE.ID_OBS2, CPI.ID_OBS, LOCAL.ID_OBS} ; if this is LOCAL, {}. * * @return the private foreign fields pointing to this table. */ public final synchronized Set<SQLField> getPrivateParentReferentFields() { this.initRF(); return this.privateParentRF; } /** * Specify the tables whose rows are contained in rows of this element. They can be specified * with table names, in which case there must be exactly one foreign field from the specified * table to this element (eg "BATIMENT" for element SITE). Otherwise it must the fullname of * foreign field which points to the table of this element (eg "RECEPTEUR.ID_LOCAL"). * * @return a Set of String. * @see #getParentFFName() */ protected Set<String> getChildren() { return Collections.emptySet(); } // *** ff public final synchronized Set<String> getNormalForeignFields() { this.initFF(); return this.normalFF; } public final synchronized Set<String> getSharedForeignFields() { this.initFF(); return this.sharedFF; } public final SQLField getParentForeignField() { return getOptionalField(this.getParentForeignFieldName()); } public final synchronized String getParentForeignFieldName() { this.initFF(); return this.parentFF; } private final SQLField getParentFF() { return getOptionalField(getParentFFName()); } // optional but if specified it must exist private final SQLField getOptionalField(final String name) { return name == null ? null : this.getTable().getField(name); } /** * Should be overloaded to specify our parent. NOTE the relationship must be specified only once * either with this method or with {@link #getChildren()}. This method is preferred since it * avoids writing IFs to account for customer differences and there's no ambiguity (you return a * field of this table instead of a table name that must be searched in roots and then a foreign * key must be found). * * @return <code>null</code> for this implementation. * @see #getChildren() */ protected String getParentFFName() { return null; } public final SQLElement getParentElement() { if (this.getParentForeignFieldName() == null) return null; else return this.getForeignElement(this.getParentForeignFieldName()); } private final synchronized Map<String, SQLElement> getPrivateFF() { this.initFF(); return this.privateFF; } /** * The fields that private to this table, ie rows pointed by these fields are referenced only by * one row of this table. * * @return private fields of this element. */ public final Set<String> getPrivateForeignFields() { return Collections.unmodifiableSet(this.getPrivateFF().keySet()); } public final SQLElement getPrivateElement(String foreignField) { return this.getPrivateFF().get(foreignField); } /** * The graph of this table and its private fields. * * @return a rowValues of this element's table with rowValues for each private foreign field. */ public final SQLRowValues getPrivateGraph() { final SQLRowValues res = new SQLRowValues(this.getTable()); res.setAllToNull(); for (final Entry<String, SQLElement> e : this.getPrivateFF().entrySet()) { res.put(e.getKey(), e.getValue().getPrivateGraph()); } return res; } /** * Renvoie les champs qui sont 'priv' cd que les ligne pointes par ce champ ne sont * rfrences que par une et une seule ligne de cette table. Cette implementation renvoie une * liste vide. This method is intented for subclasses, call {@link #getPrivateForeignFields()} * which does some checks. * * @return la List des noms des champs privs, eg ["ID_OBSERVATION_2"]. */ protected List<String> getPrivateFields() { return Collections.emptyList(); } public final void clearPrivateFields(SQLRowValues rowVals) { for (String s : getPrivateFF().keySet()) { rowVals.remove(s); } } final Map<String, ReferenceAction> getActions() { this.initFF(); return this.actions; } /** * Specify an action for a normal foreign field. * * @param ff the foreign field name. * @param action what to do if a referenced row must be archived. * @throws IllegalArgumentException if <code>ff</code> is not a normal foreign field. */ public final void setAction(final String ff, ReferenceAction action) throws IllegalArgumentException { // shared must be RESTRICT, parent at least CASCADE (to avoid child without a parent), // normal is free if (action.compareTo(ReferenceAction.RESTRICT) < 0 && !this.getNormalForeignFields().contains(ff)) // getField() checks if the field exists throw new IllegalArgumentException(getTable().getField(ff).getSQLName() + " is not a normal foreign field : " + this.getNormalForeignFields()); this.getActions().put(ff, action); } // *** rf and ff /** * The links towards the parents (either {@link #getParentForeignFieldName()} or * {@link #getPrivateParentReferentFields()}) of this element. * * @return the graph links towards the parents of this element. */ public final Set<Link> getParentsLinks() { final Set<SQLField> refFields = this.getPrivateParentReferentFields(); final Set<Link> res = new HashSet<Link>(refFields.size()); final DatabaseGraph graph = this.getTable().getDBSystemRoot().getGraph(); for (final SQLField refField : refFields) res.add(graph.getForeignLink(refField)); final SQLField parentFF = this.getParentForeignField(); if (parentFF != null) res.add(graph.getForeignLink(parentFF)); return res; } /** * The elements beneath this, ie both children and privates. * * @return our children elements. */ public final Set<SQLElement> getChildrenElements() { final Set<SQLElement> res = new HashSet<SQLElement>(); res.addAll(this.getPrivateFF().values()); for (final SQLTable child : new SQLFieldsSet(this.getChildrenReferentFields()).getTables()) res.add(getElement(child)); return res; } public final SQLElement getChildElement(final String tableName) { final SQLField field = CollectionUtils .getSole(new SQLFieldsSet(this.getChildrenReferentFields()).getFields(tableName)); if (field == null) throw new IllegalStateException("no child table named " + tableName); else return this.getElement(field.getTable()); } /** * The tables beneath this. * * @return our descendants, including this. * @see #getChildrenElements() */ public final Set<SQLTable> getDescendantTables() { final Set<SQLTable> res = new HashSet<SQLTable>(); this.getDescendantTables(res); return res; } private final void getDescendantTables(Set<SQLTable> res) { res.add(this.getTable()); for (final SQLElement elem : this.getChildrenElements()) { res.addAll(elem.getDescendantTables()); } } // *** request public final ComboSQLRequest getComboRequest() { return getComboRequest(false); } /** * Return a combo request for this element. * * @param create <code>true</code> if a new instance should be returned, <code>false</code> to * return a shared instance. * @return a combo request for this. */ public final ComboSQLRequest getComboRequest(final boolean create) { if (!create) { if (this.combo == null) { this.combo = this.createComboRequest(); } return this.combo; } else { return this.createComboRequest(); } } protected ComboSQLRequest createComboRequest() { return new ComboSQLRequest(this.getTable(), this.getComboFields()); } // not all elements need to be displayed in combos so don't make this method abstract protected List<String> getComboFields() { return this.getListFields(); } public final synchronized ListSQLRequest getListRequest() { if (this.list == null) { this.list = createListRequest(); } return this.list; } protected ListSQLRequest createListRequest() { return new ListSQLRequest(this.getTable(), this.getListFields()); } public final SQLTableModelSourceOnline getTableSource() { return this.getTableSource(!cacheTableSource()); } /** * Return a table source for this element. * * @param create <code>true</code> if a new instance should be returned, <code>false</code> to * return a shared instance. * @return a table source for this. */ public final synchronized SQLTableModelSourceOnline getTableSource(final boolean create) { if (!create) { if (this.tableSrc == null) { this.tableSrc = createAndInitTableSource(); } return this.tableSrc; } else return this.createAndInitTableSource(); } public final SQLTableModelSourceOnline createTableSource(final List<String> fields) { return initTableSource(new SQLTableModelSourceOnline(new ListSQLRequest(this.getTable(), fields))); } public final SQLTableModelSourceOnline createTableSource(final Where w) { final SQLTableModelSourceOnline res = this.getTableSource(true); res.getReq().setWhere(w); return res; } private final SQLTableModelSourceOnline createAndInitTableSource() { final SQLTableModelSourceOnline res = this.createTableSource(); res.getColumns().addAll(this.additionalListCols); return initTableSource(res); } protected synchronized void _initTableSource(final SQLTableModelSourceOnline res) { } public final synchronized SQLTableModelSourceOnline initTableSource(final SQLTableModelSourceOnline res) { // do init first since it can modify the columns this._initTableSource(res); // setEditable(false) on read only fields // MAYBE setReadOnlyFields() on SQLTableModelSource, so that SQLTableModelLinesSource can // check in commit() final Set<String> dontModif = CollectionUtils.union(this.getReadOnlyFields(), this.getInsertOnlyFields()); for (final String f : dontModif) for (final SQLTableModelColumn col : res.getColumns(getTable().getField(f))) if (col instanceof SQLTableModelColumnPath) ((SQLTableModelColumnPath) col).setEditable(false); return res; } protected SQLTableModelSourceOnline createTableSource() { // also create a new ListSQLRequest, otherwise it's a backdoor to change the behaviour of // the new TableModelSource return new SQLTableModelSourceOnline(this.createListRequest()); } /** * Whether to cache our tableSource. * * @return <code>true</code> to call {@link #createTableSource()} only once, or * <code>false</code> to call it each time {@link #getTableSource()} is. */ protected boolean cacheTableSource() { return true; } abstract protected List<String> getListFields(); public final void addListFields(final List<String> fields) { for (final String f : fields) this.addListColumn(new SQLTableModelColumnPath(getTable().getField(f))); } public final void addListColumn(SQLTableModelColumn col) { this.additionalListCols.add(col); } public final Collection<IListeAction> getRowActions() { return this.rowActions; } public final void addRowActionsListener(final IClosure<ListChangeIndex<IListeAction>> listener) { this.rowActions.getRecipe().addListener(listener); } public final void removeRowActionsListener(final IClosure<ListChangeIndex<IListeAction>> listener) { this.rowActions.getRecipe().rmListener(listener); } public String getDescription(SQLRow fromRow) { return fromRow.toString(); } // *** iterators static interface ChildProcessor<R extends SQLRowAccessor> { public void process(R parent, SQLField joint, R child) throws SQLException; } /** * Execute <code>c</code> for each children of <code>row</code>. NOTE: <code>c</code> will be * called with <code>row</code> as its first parameter, and with its child of the same type * (SQLRow or SQLRowValues) for the third parameter. * * @param <R> type of SQLRowAccessor to use. * @param row the parent row. * @param c what to do for each children. * @param deep <code>true</code> to ignore {@link #dontDeepCopy()}. * @param archived <code>true</code> to iterate over archived children. * @throws SQLException if <code>c</code> raises an exn. */ private <R extends SQLRowAccessor> void forChildrenDo(R row, ChildProcessor<? super R> c, boolean deep, boolean archived) throws SQLException { for (final SQLField childField : this.getChildrenReferentFields()) { if (deep || !this.getElement(childField.getTable()).dontDeepCopy()) { final List<SQLRow> children = row.asRow().getReferentRows(childField, archived ? SQLSelect.ARCHIVED : SQLSelect.UNARCHIVED); // eg BATIMENT[516] for (final SQLRow child : children) { c.process(row, childField, convert(child, row)); } } } } // convert toConv to same type as row @SuppressWarnings("unchecked") private <R extends SQLRowAccessor> R convert(final SQLRow toConv, R row) { final R ch; if (row instanceof SQLRow) ch = (R) toConv; else if (row instanceof SQLRowValues) ch = (R) toConv.createUpdateRow(); else throw new IllegalStateException("SQLRowAccessor is neither SQLRow nor SQLRowValues: " + toConv); return ch; } // first the leaves private void forDescendantsDo(final SQLRow row, final ChildProcessor<SQLRow> c, final boolean deep) throws SQLException { this.forDescendantsDo(row, c, deep, true, false); } <R extends SQLRowAccessor> void forDescendantsDo(final R row, final ChildProcessor<R> c, final boolean deep, final boolean leavesFirst, final boolean archived) throws SQLException { this.check(row); this.forChildrenDo(row, new ChildProcessor<R>() { public void process(R parent, SQLField joint, R child) throws SQLException { if (!leavesFirst) c.process(parent, joint, child); getElement(child.getTable()).forDescendantsDo(child, c, deep, leavesFirst, archived); if (leavesFirst) c.process(parent, joint, child); } }, deep, archived); } void check(SQLRowAccessor row) { if (!row.getTable().equals(this.getTable())) throw new IllegalArgumentException("row must of table " + this.getTable() + " : " + row); } private void checkUndefined(SQLRow row) { this.check(row); if (row.isUndefined()) throw new IllegalArgumentException("row is undefined: " + row); } // *** copy public final SQLRow copyRecursive(int id) throws SQLException { return this.copyRecursive(this.getTable().getRow(id)); } public final SQLRow copyRecursive(SQLRow row) throws SQLException { return this.copyRecursive(row, null); } public SQLRow copyRecursive(final SQLRow row, final SQLRow parent) throws SQLException { return this.copyRecursive(row, parent, null); } /** * Copy <code>row</code> and its children into <code>parent</code>. * * @param row which row to clone. * @param parent which parent the clone will have, <code>null</code> meaning the same than * <code>row</code>. * @param c allow one to modify the copied rows before they are inserted, can be * <code>null</code>. * @return the new copy. * @throws SQLException if an error occurs. */ public SQLRow copyRecursive(final SQLRow row, final SQLRow parent, final IClosure<SQLRowValues> c) throws SQLException { check(row); if (row.isUndefined()) return row; // current => new copy final Map<SQLRow, SQLRowValues> copies = new HashMap<SQLRow, SQLRowValues>(); return SQLUtils.executeAtomic(this.getTable().getBase().getDataSource(), new SQLFactory<SQLRow>() { @Override public SQLRow create() throws SQLException { // eg SITE[128] final SQLRowValues copy = createTransformedCopy(row, parent, c); copies.put(row, copy); forDescendantsDo(row, new ChildProcessor<SQLRow>() { public void process(SQLRow parent, SQLField joint, SQLRow desc) throws SQLException { final SQLRowValues parentCopy = copies.get(parent); if (parentCopy == null) throw new IllegalStateException("null copy of " + parent); final SQLRowValues descCopy = createTransformedCopy(desc, null, c); descCopy.put(joint.getName(), parentCopy); copies.put(desc, descCopy); } }, false, false, false); // ne pas descendre en deep // reference forDescendantsDo(row, new ChildProcessor<SQLRow>() { public void process(SQLRow parent, SQLField joint, SQLRow desc) throws SQLException { final CollectionMap<SQLField, SQLRow> normalReferents = getElement(desc.getTable()) .getNonChildrenReferents(desc); for (final Entry<SQLField, Collection<SQLRow>> e : normalReferents.entrySet()) { // eg SOURCE.ID_CPI final SQLField refField = e.getKey(); for (final SQLRow ref : e.getValue()) { // eg copy of SOURCE[12] is SOURCE[354] final SQLRowValues refCopy = copies.get(ref); if (refCopy != null) { // CPI[1203] final SQLRowValues referencedCopy = copies.get(desc); refCopy.put(refField.getName(), referencedCopy); } } } } }, false); // we used to remove foreign links pointing outside the copy, but this was almost // never right, e.g. : copy a batiment, its locals loose ID_FAMILLE ; copy a local, // if a source in it points to an item in another local, its copy won't. return copy.insert(); } }); } private final SQLRowValues createTransformedCopy(SQLRow desc, SQLRow parent, final IClosure<SQLRowValues> c) throws SQLException { final SQLRowValues copiedVals = getElement(desc.getTable()).createCopy(desc, parent); assert copiedVals != null : "failed to copy " + desc; if (c != null) c.executeChecked(copiedVals); return copiedVals; } public final SQLRow copy(int id) throws SQLException { return this.copy(this.getTable().getRow(id)); } public final SQLRow copy(SQLRow row) throws SQLException { return this.copy(row, null); } public final SQLRow copy(SQLRow row, SQLRow parent) throws SQLException { final SQLRowValues copy = this.createCopy(row, parent); return copy == null ? row : copy.insert(); } public final SQLRowValues createCopy(int id) { final SQLRow row = this.getTable().getRow(id); return this.createCopy(row, null); } /** * Copies the passed row into an SQLRowValues. NOTE: this method does not access the DB, ie the * copy won't be a copy of the current values in DB, but of the current values of the passed * instance. * * @param row the row to copy, can be <code>null</code>. * @param parent the parent the copy will be in, <code>null</code> meaning the same as * <code>row</code>. * @return a copy ready to be inserted, or <code>null</code> if <code>row</code> cannot be * copied. */ public SQLRowValues createCopy(SQLRow row, SQLRow parent) { // do NOT copy the undefined if (row == null || row.isUndefined()) return null; this.check(row); final SQLRowValues copy = new SQLRowValues(this.getTable()); this.loadAllSafe(copy, row); for (final String privateName : this.getPrivateForeignFields()) { final SQLElement privateElement = this.getPrivateElement(privateName); if (!privateElement.dontDeepCopy() && !row.isForeignEmpty(privateName)) { final SQLRowValues child = privateElement.createCopy(row.getInt(privateName)); copy.put(privateName, child); } else { copy.putEmptyLink(privateName); } } // si on a spcifi un parent, eg BATIMENT[23] if (parent != null) { final SQLTable foreignTable = this.getParentForeignField().getForeignTable(); if (!parent.getTable().equals(foreignTable)) throw new IllegalArgumentException(parent + " is not a parent of " + row); copy.put(this.getParentForeignFieldName(), parent.getID()); } return copy; } /** * Load all values that can be safely copied (shared by multiple rows). This means all values * except private, primary, order and archive. * * @param vals the row to modify. * @param row the row to be loaded. */ public final void loadAllSafe(final SQLRowValues vals, final SQLRow row) { check(vals); check(row); vals.setAll(row.getAllValues()); vals.load(row, this.getNormalForeignFields()); if (this.getParentForeignFieldName() != null) vals.put(this.getParentForeignFieldName(), row.getObject(this.getParentForeignFieldName())); vals.load(row, this.getSharedForeignFields()); } // *** getRows /** * Returns the descendant rows : the children of this element, recursively. ATTN does not carry * the hierarchy. * * @param row a SQLRow. * @return the descendant rows by SQLTable. */ public final CollectionMap<SQLTable, SQLRow> getDescendants(SQLRow row) { check(row); final CollectionMap<SQLTable, SQLRow> mm = new CollectionMap<SQLTable, SQLRow>(); try { this.forDescendantsDo(row, new ChildProcessor<SQLRow>() { public void process(SQLRow parent, SQLField joint, SQLRow child) throws SQLException { mm.put(joint.getTable(), child); } }, true); } catch (SQLException e) { // never happen e.printStackTrace(); } return mm; } /** * Returns the tree beneath the passed row. The list is ordered "leaves-first", ie the last item * is the root. * * @param row the root of the desired tree. * @param archived <code>true</code> if the returned rows should be archived. * @return a List of SQLRow. */ private List<SQLRow> getTree(SQLRow row, boolean archived) { check(row); // nos descendants final List<SQLRow> descsAndMe = new ArrayList<SQLRow>(); try { this.forDescendantsDo(row, new ChildProcessor<SQLRow>() { public void process(SQLRow parent, SQLField joint, SQLRow desc) throws SQLException { descsAndMe.add(desc); } }, true, true, archived); } catch (SQLException e) { // never happen cause process don't throw it e.printStackTrace(); } if (row.isArchived() == archived) descsAndMe.add(row); return descsAndMe; } /** * Returns the children of the passed row. * * @param row a SQLRow. * @return the children rows by SQLTable. */ public CollectionMap<SQLTable, SQLRow> getChildrenRows(SQLRow row) { check(row); // ArrayList final CollectionMap<SQLTable, SQLRow> mm = new CollectionMap<SQLTable, SQLRow>(); try { this.forChildrenDo(row, new ChildProcessor<SQLRow>() { public void process(SQLRow parent, SQLField joint, SQLRow child) throws SQLException { mm.put(child.getTable(), child); } }, true, false); } catch (SQLException e) { // never happen e.printStackTrace(); } return mm; } public SQLRowAccessor getParent(SQLRowAccessor row) { check(row); final List<SQLRowAccessor> parents = new ArrayList<SQLRowAccessor>(); for (final Link l : this.getParentsLinks()) { parents.addAll(row.followLink(l)); } if (parents.size() > 1) throw new IllegalStateException("More than one parent for " + row + " : " + parents); return parents.size() == 0 ? null : parents.get(0); } public SQLRow getForeignParent(SQLRow row) { return this.getForeignParent(row, SQLRowMode.VALID); } // ATTN cannot replace with getParent(SQLRowAccessor) since some callers assume the result to be // a foreign row (which isn't the case for private) private SQLRow getForeignParent(SQLRow row, final SQLRowMode mode) { check(row); return this.getParentForeignFieldName() == null ? null : row.getForeignRow(this.getParentForeignFieldName(), mode); } // {SQLField => List<SQLRow>} CollectionMap<SQLField, SQLRow> getNonChildrenReferents(SQLRow row) { check(row); final CollectionMap<SQLField, SQLRow> mm = new CollectionMap<SQLField, SQLRow>(); final Set<SQLField> nonChildren = new HashSet<SQLField>( row.getTable().getDBSystemRoot().getGraph().getReferentKeys(row.getTable())); nonChildren.removeAll(this.getChildrenReferentFields()); for (final SQLField refField : nonChildren) { // eg CONTACT.ID_SITE => [CONTACT[12], CONTACT[13]] mm.putAll(refField, row.getReferentRows(refField)); } return mm; } public Map<String, SQLRow> getNormalForeigns(SQLRow row) { return this.getNormalForeigns(row, SQLRowMode.DEFINED); } private Map<String, SQLRow> getNormalForeigns(SQLRow row, final SQLRowMode mode) { check(row); final Map<String, SQLRow> mm = new HashMap<String, SQLRow>(); final Iterator<String> iter = this.getNormalForeignFields().iterator(); while (iter.hasNext()) { // eg SOURCE.ID_CPI final String ff = iter.next(); // eg CPI[12] final SQLRow foreignRow = row.getForeignRow(ff, mode); if (foreignRow != null) mm.put(ff, foreignRow); } return mm; } /** * Returns a java object modeling the passed row. * * @param row the row to model. * @return an instance modeling the passed row or <code>null</code> if there's no class to model * this table. * @see SQLRowAccessor#getModelObject() */ public Object getModelObject(SQLRowAccessor row) { check(row); if (this.getModelClass() == null) return null; final Object res; // seuls les SQLRow peuvent tre caches if (row instanceof SQLRow) { // MAYBE make the modelObject change final CacheResult<Object> cached = this.getModelCache().check(row); if (cached.getState() == CacheResult.State.NOT_IN_CACHE) { res = this.createModelObject(row); this.getModelCache().put(row, res, Collections.singleton(row)); } else res = cached.getRes(); } else res = this.createModelObject(row); return res; } private final Object createModelObject(SQLRowAccessor row) { if (!RowBacked.class.isAssignableFrom(this.getModelClass())) throw new IllegalStateException("modelClass must inherit from RowBacked: " + this.getModelClass()); final Constructor<? extends RowBacked> ctor; try { ctor = this.getModelClass().getConstructor(new Class[] { SQLRowAccessor.class }); } catch (Exception e) { throw ExceptionUtils.createExn(IllegalStateException.class, "no SQLRowAccessor constructor", e); } try { return ctor.newInstance(new Object[] { row }); } catch (Exception e) { throw ExceptionUtils.createExn(RuntimeException.class, "pb creating instance", e); } } protected Class<? extends RowBacked> getModelClass() { return null; } // *** equals public boolean equals(SQLRow row, SQLRow row2) throws SQLException { check(row); if (!row2.getTable().equals(this.getTable())) return false; if (row.equals(row2)) return true; // the same table but not the same id if (!row.getAllValues().equals(row2.getAllValues())) return false; // shared doivent tre partages (!) for (final String shared : this.getSharedForeignFields()) { if (row.getInt(shared) != row2.getInt(shared)) return false; } // les private equals for (final String prvt : this.getPrivateForeignFields()) { final SQLElement foreignElement = this.getForeignElement(prvt); // ne pas tester if (!foreignElement.dontDeepCopy() && !foreignElement.equals(row.getForeignRow(prvt), row2.getForeignRow(prvt))) return false; } return true; } public boolean equalsRecursive(SQLRow row, SQLRow row2) throws SQLException { // if (!equals(row, row2)) // return false; return new SQLElementRowR(this, row).equals(new SQLElementRowR(this, row2)); } @Override public final boolean equals(Object obj) { if (obj instanceof SQLElement) { final SQLElement o = (SQLElement) obj; // don't need to compare SQLField computed by initFF() & initRF() since they're function // of this.table (and by extension its graph) & this.directory final boolean parentEq = CompareUtils.equals(this.getParentFFName(), o.getParentFFName()); return this.getTable().equals(o.getTable()) && CompareUtils.equals(this.getDirectory(), o.getDirectory()) && parentEq && this.getPrivateFields().equals(o.getPrivateFields()) && this.getChildren().equals(o.getChildren()); } else { return false; } } @Override public synchronized int hashCode() { final int prime = 31; int result = 1; result = prime * result + this.primaryTable.hashCode(); result = prime * result + ((this.directory == null) ? 0 : this.directory.hashCode()); return result; } @Override public String toString() { return this.getClass().getName() + " " + this.getTable().getSQLName(); } // *** gui public final void addComponentFactory(final String id, final ITransformer<Tuple2<SQLElement, String>, SQLComponent> t) { if (t == null) throw new NullPointerException(); this.components.put(id, t); } public final void removeComponentFactory(final String id, final ITransformer<Tuple2<SQLElement, String>, SQLComponent> t) { if (t == null) throw new NullPointerException(); this.components.remove(id, t); } private final SQLComponent createComponent(final String id, final boolean defaultItem) { final String actualID = defaultItem ? DEFAULT_COMP_ID : id; final Tuple2<SQLElement, String> t = Tuple2.create(this, id); // start from the most recently added factory final Iterator<ITransformer<Tuple2<SQLElement, String>, SQLComponent>> iter = ((LinkedList<ITransformer<Tuple2<SQLElement, String>, SQLComponent>>) this.components .getNonNull(actualID)).descendingIterator(); while (iter.hasNext()) { final SQLComponent res = iter.next().transformChecked(t); if (res != null) return res; } return null; } public final SQLComponent createDefaultComponent() { return this.createComponent(DEFAULT_COMP_ID); } /** * Create the component for the passed ID. First factories for the passed ID are executed, after * that if ID is the {@link #DEFAULT_COMP_ID default} then {@link #createComponent()} is called * else factories for {@link #DEFAULT_COMP_ID} are executed. * * @param id the requested ID. * @return the component or <code>null</code> if all factories return <code>null</code>. */ public final SQLComponent createComponent(final String id) { SQLComponent res = this.createComponent(id, false); if (res == null) { if (CompareUtils.equals(id, DEFAULT_COMP_ID)) { // since we don't pass id to this method, only call it for DEFAULT_ID res = this.createComponent(); } else { res = this.createComponent(id, true); } } res.setCode(id); return res; } /** * Retourne l'interface graphique de saisie. * * @return l'interface graphique de saisie. */ protected abstract SQLComponent createComponent(); public final void addToMDPath(final String mdVariant) { if (mdVariant == null) throw new NullPointerException(); synchronized (this) { final LinkedList<String> newL = new LinkedList<String>(this.mdPath); newL.addFirst(mdVariant); this.mdPath = Collections.unmodifiableList(newL); } } public synchronized final void removeFromMDPath(final String mdVariant) { final LinkedList<String> newL = new LinkedList<String>(this.mdPath); if (newL.remove(mdVariant)) this.mdPath = Collections.unmodifiableList(newL); } /** * The variants searched to find item metadata by * {@link SQLFieldTranslator#getDescFor(SQLTable, String, String)}. This allow to configure this * element to choose between the simultaneously loaded metadata. * * @return the variants path. */ public synchronized final List<String> getMDPath() { return this.mdPath; } /** * Allows a module to add a view for a field to this element. * * @param field the field of the component. * @return <code>true</code> if no view existed. */ public final boolean putAdditionalField(final String field) { return this.putAdditionalField(field, (JComponent) null); } public final boolean putAdditionalField(final String field, final JTextComponent comp) { return this.putAdditionalField(field, (JComponent) comp); } public final boolean putAdditionalField(final String field, final SQLTextCombo comp) { return this.putAdditionalField(field, (JComponent) comp); } // private as only a few JComponent are OK private final boolean putAdditionalField(final String field, final JComponent comp) { if (this.additionalFields.containsKey(field)) { return false; } else { this.additionalFields.put(field, comp); return true; } } public final Map<String, JComponent> getAdditionalFields() { return Collections.unmodifiableMap(this.additionalFields); } public final void removeAdditionalField(final String field) { this.additionalFields.remove(field); } public final boolean askArchive(final Component comp, final Number ids) { return this.askArchive(comp, Collections.singleton(ids)); } /** * Ask to the user before archiving. * * @param comp the parent component. * @param ids which rows to archive. * @return <code>true</code> if the rows were successfully archived, <code>false</code> * otherwise. */ public boolean askArchive(final Component comp, final Collection<? extends Number> ids) { boolean shouldArchive = false; final int rowCount = ids.size(); if (rowCount == 0) return true; try { if (!UserRightsManager.getCurrentUserRights().canDelete(getTable())) throw new SQLException("forbidden"); final TreesOfSQLRows trees = TreesOfSQLRows.createFromIDs(this, ids); final CollectionMap<SQLTable, SQLRowAccessor> descs = trees.getDescendantsByTable(); final SortedMap<SQLField, Integer> externRefs = trees.getExternReferencesCount(); final String confirmDelete = getTM().trA("sqlElement.confirmDelete"); final Map<String, Object> map = new HashMap<String, Object>(); map.put("rowCount", rowCount); final int descsSize = descs.size(); final int externsSize = externRefs.size(); if (descsSize + externsSize > 0) { final String descsS = descsSize > 0 ? toString(descs) : null; final String externsS = externsSize > 0 ? toStringExtern(externRefs) : null; map.put("descsSize", descsSize); map.put("descs", descsS); map.put("externsSize", externsSize); map.put("externs", externsS); map.put("times", "once"); int i = askSerious(comp, getTM().trM("sqlElement.deleteRef.details", map) + getTM().trM("sqlElement.deleteRef", map), confirmDelete); if (i == JOptionPane.YES_OPTION) { map.put("times", "twice"); final String msg = externsSize > 0 ? getTM().trM("sqlElement.deleteRef.details2", map) : ""; i = askSerious(comp, msg + getTM().trM("sqlElement.deleteRef", map), confirmDelete); if (i == JOptionPane.YES_OPTION) { shouldArchive = true; } else { JOptionPane.showMessageDialog(comp, getTM().trA("sqlElement.noLinesDeleted"), getTM().trA("sqlElement.noLinesDeletedTitle"), JOptionPane.INFORMATION_MESSAGE); } } } else { int i = askSerious(comp, getTM().trM("sqlElement.deleteNoRef", map), confirmDelete); if (i == JOptionPane.YES_OPTION) { shouldArchive = true; } } if (shouldArchive) { this.archive(trees, true); return true; } else return false; } catch (SQLException e) { ExceptionHandler.handle(comp, TM.tr("sqlElement.archiveError", this, ids), e); return false; } } @SuppressWarnings("rawtypes") private final String toString(MultiMap descs) { final List<String> l = new ArrayList<String>(); final Iterator iter = descs.keySet().iterator(); while (iter.hasNext()) { final SQLTable t = (SQLTable) iter.next(); final Collection rows = (Collection) descs.get(t); final SQLElement elem = getElement(t); l.add(elemToString(rows.size(), elem)); } return CollectionUtils.join(l, "\n"); } private static final String elemToString(int count, SQLElement elem) { return "- " + elem.getName().getNumeralVariant(count, Grammar.INDEFINITE_NUMERAL); } // traduire TRANSFO.ID_ELEMENT_TABLEAU_PRI -> {TRANSFO[5], TRANSFO[12]} // en 2 transformateurs vont perdre leurs champs 'Circuit primaire' private final String toStringExtern(SortedMap<SQLField, Integer> externRef) { final List<String> l = new ArrayList<String>(); final Map<String, Object> map = new HashMap<String, Object>(4); for (final Map.Entry<SQLField, Integer> entry : externRef.entrySet()) { final SQLField foreignKey = entry.getKey(); final int count = entry.getValue(); final String label = Configuration.getTranslator(foreignKey.getTable()).getLabelFor(foreignKey); final SQLElement elem = getElement(foreignKey.getTable()); map.put("elementName", elem.getName()); map.put("count", count); map.put("linkName", label); l.add(getTM().trM("sqlElement.linksWillBeCut", map)); } return CollectionUtils.join(l, "\n"); } private final int askSerious(Component comp, String msg, String title) { return JOptionPane.showConfirmDialog(comp, msg, title + " (" + this.getPluralName() + ")", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); } }