Dépôt officiel du code source de l'ERP OpenConcerto
Rev 174 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
/*
* 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.FieldExpander;
import org.openconcerto.sql.Log;
import org.openconcerto.sql.PropsConfiguration;
import org.openconcerto.sql.TM;
import org.openconcerto.sql.element.SQLElementLink.LinkType;
import org.openconcerto.sql.element.TreesOfSQLRows.LinkToCut;
import org.openconcerto.sql.model.DBStructureItemNotFound;
import org.openconcerto.sql.model.SQLField;
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.SQLRowValues.CreateMode;
import org.openconcerto.sql.model.SQLRowValues.ForeignCopyMode;
import org.openconcerto.sql.model.SQLRowValuesCluster;
import org.openconcerto.sql.model.SQLRowValuesCluster.DiffResult;
import org.openconcerto.sql.model.SQLRowValuesCluster.State;
import org.openconcerto.sql.model.SQLRowValuesCluster.StopRecurseException;
import org.openconcerto.sql.model.SQLRowValuesCluster.StoreMode;
import org.openconcerto.sql.model.SQLRowValuesCluster.WalkOptions;
import org.openconcerto.sql.model.SQLRowValuesListFetcher;
import org.openconcerto.sql.model.SQLSelect;
import org.openconcerto.sql.model.SQLSelect.ArchiveMode;
import org.openconcerto.sql.model.SQLSelect.LockStrength;
import org.openconcerto.sql.model.SQLSyntax;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.SQLTable.FieldGroup;
import org.openconcerto.sql.model.SQLTable.VirtualFields;
import org.openconcerto.sql.model.Where;
import org.openconcerto.sql.model.graph.Link;
import org.openconcerto.sql.model.graph.Link.Direction;
import org.openconcerto.sql.model.graph.Path;
import org.openconcerto.sql.model.graph.PathBuilder;
import org.openconcerto.sql.model.graph.SQLKey;
import org.openconcerto.sql.model.graph.SQLKey.Type;
import org.openconcerto.sql.model.graph.Step;
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.SQLRequestComboBox;
import org.openconcerto.sql.sqlobject.SQLTextCombo;
import org.openconcerto.sql.ui.light.CustomRowEditor;
import org.openconcerto.sql.ui.light.GroupToLightUIConvertor;
import org.openconcerto.sql.ui.light.LightEditFrame;
import org.openconcerto.sql.ui.light.LightUIPanelFiller;
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.EditFrame;
import org.openconcerto.sql.view.EditPanel.EditMode;
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.SQLTableModelSource;
import org.openconcerto.sql.view.list.SQLTableModelSourceOffline;
import org.openconcerto.sql.view.list.SQLTableModelSourceOnline;
import org.openconcerto.ui.group.Group;
import org.openconcerto.ui.light.ComboValueConvertor;
import org.openconcerto.ui.light.IntValueConvertor;
import org.openconcerto.ui.light.LightUIComboBox;
import org.openconcerto.ui.light.LightUIElement;
import org.openconcerto.ui.light.LightUIFrame;
import org.openconcerto.ui.light.LightUIPanel;
import org.openconcerto.ui.light.StringValueConvertor;
import org.openconcerto.utils.CollectionMap2Itf.SetMapItf;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.ExceptionHandler;
import org.openconcerto.utils.ExceptionUtils;
import org.openconcerto.utils.LinkedListMap;
import org.openconcerto.utils.ListMap;
import org.openconcerto.utils.NumberUtils;
import org.openconcerto.utils.RTInterruptedException;
import org.openconcerto.utils.RecursionType;
import org.openconcerto.utils.ReflectUtils;
import org.openconcerto.utils.SetMap;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.utils.Value;
import org.openconcerto.utils.cache.CacheResult;
import org.openconcerto.utils.cc.IClosure;
import org.openconcerto.utils.cc.ITransformer;
import org.openconcerto.utils.cc.Transformer;
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.math.BigDecimal;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.function.Supplier;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.text.JTextComponent;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.Immutable;
/**
* Décrit comment manipuler un élément de la BD (pas forcément une seule table, voir
* privateForeignField).
*
* @author ilm
*/
public abstract class SQLElement {
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.putVariantIfDifferent(Grammar.INDEFINITE_ARTICLE_SINGULAR, singular);
res.putVariantIfDifferent(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 Phrase defaultName;
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 LinkedListMap<String, ITransformer<Tuple2<SQLElement, String>, SQLComponent>> components;
// links
private SQLElementLinks ownedLinks;
private SQLElementLinks otherLinks;
// keep it for now as joins are disallowed (see initFF())
private String parentFF;
// lazy creation
@GuardedBy("this")
private SQLCache<SQLRow, Object> modelCache;
private final Map<String, Supplier<? extends JComponent>> additionalFields;
private final List<SQLTableModelColumn> additionalListCols;
@GuardedBy("this")
private List<String> mdPath;
private Group defaultGroup;
private Group groupForCreation;
private Group groupForModification;
@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.setDefaultName(name);
this.code = code == null ? createCode() : code;
this.combo = null;
this.list = null;
this.rowActions = new ListChangeRecorder<IListeAction>(new ArrayList<IListeAction>());
this.resetRelationships();
this.components = new LinkedListMap<String, ITransformer<Tuple2<SQLElement, String>, SQLComponent>>();
this.modelCache = null;
// the components should always be in the same order
this.additionalFields = new LinkedHashMap<>();
this.additionalListCols = new ArrayList<SQLTableModelColumn>();
this.mdPath = Collections.emptyList();
}
public void destroy() {
}
/**
* 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>. This implementation returns a string
* containing the {@link Class#getName() full name} of the class and the {@link #getTable()
* table} name to handle a single class being used for multiple tables. NOTE: this method is
* also needed, since a subclass constructor cannot pass <code>this.getClass().getName()</code>
* as the code parameter to <code>super</code>.
*
* @return the default code for this element.
*/
protected String createCode() {
return getClass().getName() + "-" + getTable().getName();
}
public Group getGroupForCreation() {
if (this.groupForCreation != null) {
return this.groupForCreation;
}
return getDefaultGroup();
}
public Group getGroupForModification() {
if (this.groupForModification != null) {
return this.groupForModification;
}
return getDefaultGroup();
}
public Group getDefaultGroup() {
return this.defaultGroup;
}
public void setDefaultGroup(Group defaultGroup) {
this.defaultGroup = defaultGroup;
}
/**
* Get the group based on the edit mode
*
* @param editMode
* @return
*/
public Group getEditGroup(final EditMode editMode) {
if (editMode.equals(EditMode.CREATION)) {
return this.getGroupForCreation();
} else {
return this.getGroupForModification();
}
}
/**
* Override this function in an element to show default values in edit frame
*
* @param token - The security token of session
*
* @return a default SQLRowValues
*/
public SQLRowValues createDefaultRowValues(final String token) {
return new SQLRowValues(getTable());
}
/**
* Create the edition frame for this SQLElement
*
* @param configuration current configuration
* @param parentFrame parent frame of the edit frame
* @param editMode edition mode (CREATION, MODIFICATION, READONLY)
* @param sqlRow SQLRowAccessor use for fill the edition frame
* @param sessionSecurityToken String, use for find session with an instance of LightServer
* @return the edition frame of this SQLElement
*/
public LightEditFrame createEditFrame(final PropsConfiguration configuration, final LightUIFrame parentFrame, final EditMode editMode, final SQLRowAccessor sqlRow,
final String sessionSecurityToken) {
final Group editGroup = this.getEditGroup(editMode);
if (editGroup == null) {
Log.get().severe("The edit group is null for this element : " + this);
return null;
}
final GroupToLightUIConvertor convertor = this.getGroupToLightUIConvertor(configuration, editMode, sqlRow, sessionSecurityToken);
final LightEditFrame editFrame = convertor.convert(editGroup, sqlRow, parentFrame, editMode);
if (editMode.equals(EditMode.CREATION)) {
editFrame.createTitlePanel(this.getCreationFrameTitle());
} else if (editMode.equals(EditMode.MODIFICATION)) {
editFrame.createTitlePanel(this.getModificationFrameTitle(sqlRow));
new LightUIPanelFiller(editFrame.getContentPanel()).fillFromRow(configuration, this, sqlRow, sessionSecurityToken);
} else if (editMode.equals(EditMode.READONLY)) {
editFrame.createTitlePanel(this.getReadOnlyFrameTitle(sqlRow));
new LightUIPanelFiller(editFrame.getContentPanel()).fillFromRow(configuration, this, sqlRow, sessionSecurityToken);
}
this.setEditFrameModifiers(editFrame, sessionSecurityToken);
return editFrame;
}
/**
* Get title for read only mode
*
* @param sqlRow - SQLRowValues use for fill the edition frame
* @return The title for read only mode
*/
protected String getReadOnlyFrameTitle(final SQLRowAccessor sqlRow) {
return EditFrame.getReadOnlyMessage(this);
}
/**
* Get title for modification mode
*
* @param sqlRow - SQLRowValues use for fill the edition frame
* @return The title for read only mode
*/
protected String getModificationFrameTitle(final SQLRowAccessor sqlRow) {
return EditFrame.getModifyMessage(this);
}
/**
* Get title for creation mode
*
* @param sqlRow - SQLRowValues use for fill the edition frame
* @return The title for read only mode
*/
protected String getCreationFrameTitle() {
return EditFrame.getCreateMessage(this);
}
/**
*
* @param configuration - The user SQL configuration
* @param editMode - Edit mode of the frame
* @param sqlRow - The row to update
* @param token - The session security token
*
* @return An initialized GroupToLightUIConvertor
*/
public GroupToLightUIConvertor getGroupToLightUIConvertor(final PropsConfiguration configuration, final EditMode editMode, final SQLRowAccessor sqlRow, final String token) {
final GroupToLightUIConvertor convertor = new GroupToLightUIConvertor(configuration);
// if (editMode.equals(EditMode.CREATION)) {
convertor.putAllCustomEditorProvider(this.getCustomRowEditors(configuration, token));
// } else {
// convertor.putAllCustomEditorProvider(this.getCustomRowEditors(configuration, sqlRow,
// token));
// }
return convertor;
}
/**
* Override this function in an element and put new value in map for use ComboValueConvertor.
* This one allow you to change value store in database by an other one.
*
* @return Map which contains all ComboValueConvertors use for this SQLElement edition. Key: ID
* of group item / Value: ComboValueConvertor
*/
// TODO: use renderer instead of ValueConvertor
public Map<String, ComboValueConvertor<?>> getComboConvertors() {
return new HashMap<String, ComboValueConvertor<?>>();
}
/**
* Override this function in an element and put new value in map for use ConvertorModifier. This
* one allow you to apply some change on LightUIElement before send it to client
*
*/
// TODO: implement with IClosure
public void setEditFrameModifiers(final LightEditFrame frame, final String sessionToken) {
}
public List<CustomRowEditor> getCustomRowEditors(final Configuration configuration, final String sessionToken) {
final Map<String, ComboValueConvertor<?>> comboConvertors = this.getComboConvertors();
final List<CustomRowEditor> result = new ArrayList<>();
for (final Entry<String, ComboValueConvertor<?>> entry : comboConvertors.entrySet()) {
final String itemId = entry.getKey();
result.add(new CustomRowEditor(itemId) {
final ComboValueConvertor<?> convertor = entry.getValue();
@Override
public LightUIElement createUIElement() {
final LightUIComboBox uiCombo = new LightUIComboBox(getItemId());
this.convertor.fillCombo(uiCombo, null);
return uiCombo;
}
@Override
public void fillFrom(LightUIElement uiElement, SQLRowAccessor sqlRow) {
final LightUIComboBox uiCombo = (LightUIComboBox) uiElement;
final SQLField field = configuration.getFieldMapper().getSQLFieldForItem(getItemId());
if (this.convertor instanceof StringValueConvertor) {
((StringValueConvertor) this.convertor).fillCombo(uiCombo, sqlRow.getString(field.getFieldName()));
} else if (this.convertor instanceof IntValueConvertor) {
if (sqlRow.getObject(field.getFieldName()) != null) {
((IntValueConvertor) this.convertor).fillCombo(uiCombo, sqlRow.getInt(field.getFieldName()));
}
}
}
@Override
public void store(LightUIElement uiElement, SQLRowValues sqlRow) {
final LightUIComboBox combobox = (LightUIComboBox) uiElement;
final String fieldName = configuration.getFieldMapper().getSQLFieldForItem(getItemId()).getName();
if (combobox.hasSelectedValue()) {
if (this.convertor instanceof StringValueConvertor) {
sqlRow.put(fieldName, ((StringValueConvertor) this.convertor).getIdFromIndex(combobox.getSelectedValue().getId()));
} else if (this.convertor instanceof IntValueConvertor) {
sqlRow.put(fieldName, combobox.getSelectedValue().getId());
} else {
throw new IllegalArgumentException("the save is not implemented for the class: " + this.convertor.getClass().getName() + " - ui id: " + getItemId());
}
} else {
sqlRow.put(fieldName, null);
}
}
});
}
return result;
}
/**
* Override this function in an element to execute some code just after inserted new row in
* database
*
* @param editFrame - The edit frame of this SQLRow
* @param sqlRow - The row which was just inserted
* @param sessionToken Security token of session which allow to find session in LightServer
* instance
*
* @throws Exception
*/
public void doAfterLightInsert(final LightEditFrame editFrame, final SQLRow sqlRow, final String sessionToken) throws Exception {
}
/**
* Override this function in an element to execute some code just after deleted a row in
* database
*
* @param frame - The current frame
* @param sqlRow - The row which was deleted
* @param sessionToken Security token of session which allow to find session in LightServer
* instance
*
* @throws Exception
*/
public void doAfterLightDelete(final LightUIFrame frame, final SQLRowValues sqlRow, final String sessionToken) throws Exception {
}
/**
* Override this function in an element to execute some code before inserted new row in database
*
* @param frame - The current frame
* @param sqlRow - The row which will be deleted
* @param sessionToken - Security token of session which allow to find session in LightServer
* instance
*
* @throws Exception
*/
public void doBeforeLightDelete(final LightUIFrame frame, final SQLRowValues sqlRow, final String sessionToken) throws Exception {
}
/**
* Override this function in an element to execute some code before inserted new row in database
*
* @param editFrame - The edit frame of this SQLRowValues
* @param sqlRow - The row which was just inserted
* @param sessionToken - Security token of session which allow to find session in LightServer
* instance
*
* @throws Exception
*/
public void doBeforeLightInsert(final LightEditFrame editFrame, final SQLRowValues sqlRow, final String sessionToken) throws Exception {
}
/**
* Get ShowAs values of this SQLElement
*
* @param id - The id which you want to expand
*
* @return A SQLRowValues with data
*/
public SQLRowValues getValuesOfShowAs(final Number id) {
final SQLRowValues tmp = new SQLRowValues(this.getTable());
final ListMap<String, String> showAs = this.getShowAs();
for (final List<String> listStr : showAs.values()) {
tmp.putNulls(listStr);
}
this.getDirectory().getShowAs().expand(tmp);
final SQLRowValues fetched = SQLRowValuesListFetcher.create(tmp).fetchOne(id);
if (fetched == null) {
throw new IllegalArgumentException("Impossible to find Row in database - table: " + this.getTable().getName() + ", id: " + id);
}
return fetched;
}
/**
* Must be called if foreign/referent keys are added or removed.
*/
public synchronized void resetRelationships() {
if (this.areRelationshipsInited()) {
// if we remove links, notify owned elements
for (final SQLElementLink l : this.ownedLinks.getByPath().values()) {
l.getOwned().resetRelationshipsOf(this);
}
}
this.ownedLinks = this instanceof JoinSQLElement ? new SQLElementLinks(SetMap.<LinkType, SQLElementLink> empty()) : null;
this.otherLinks = this instanceof JoinSQLElement ? new SQLElementLinks(SetMap.<LinkType, SQLElementLink> empty()) : null;
this.parentFF = null;
}
private synchronized void resetRelationshipsOf(final SQLElement changed) {
// MAYBE optimize and only remove links for the passed argument
this.otherLinks = null;
}
protected synchronized final boolean areRelationshipsInited() {
return this.ownedLinks != null;
}
// return Path from owner to owned
private final Set<Path> createPaths(final boolean wantedOwned) {
// joins cannot have SQLElementLink
if (this instanceof JoinSQLElement)
return Collections.emptySet();
final SQLTable thisTable = this.getTable();
final Set<Link> allLinks = thisTable.getDBSystemRoot().getGraph().getAllLinks(getTable());
final Set<Path> res = new HashSet<Path>();
for (final Link l : allLinks) {
final boolean owned;
final Path pathFromOwner;
final SQLElement sourceElem = this.getElementLenient(l.getSource());
if (sourceElem instanceof JoinSQLElement) {
final JoinSQLElement joinElem = (JoinSQLElement) sourceElem;
pathFromOwner = joinElem.getPathFromOwner();
// ATTN when source == target, the same path will both owned and not
owned = joinElem.getLinkToOwner().equals(l);
} else if (l.getSource() == l.getTarget()) {
owned = wantedOwned;
pathFromOwner = new PathBuilder(l.getSource()).add(l, Direction.FOREIGN).build();
} else {
owned = l.getSource() == thisTable;
pathFromOwner = new PathBuilder(l.getSource()).add(l).build();
}
if (owned == wantedOwned)
res.add(pathFromOwner);
}
return res;
}
// this implementation uses getParentFFName() and assumes that links to privates are COMPOSITION
final SetMap<LinkType, Path> getDefaultLinkTypes() {
final Set<Path> ownedPaths = createPaths(true);
final SetMap<LinkType, Path> res = new SetMap<LinkType, Path>();
final String parentFFName = getParentFFName();
if (parentFFName != null) {
final Path pathToParent = new PathBuilder(getTable()).addForeignField(parentFFName).build();
if (!ownedPaths.remove(pathToParent))
throw new IllegalStateException("getParentFFName() " + pathToParent + " isn't in " + ownedPaths);
res.add(LinkType.PARENT, pathToParent);
}
final List<String> privateFields = this.getPrivateFields();
if (!privateFields.isEmpty())
Log.get().warning("getPrivateFields() is deprecated use setupLinks(), " + this + " : " + privateFields);
// links to private are COMPOSITION by default (normal links to privates are few)
final Iterator<Path> iter = ownedPaths.iterator();
while (iter.hasNext()) {
final Path ownedPath = iter.next();
if (getElement(ownedPath.getLast()).isPrivate()) {
iter.remove();
res.add(LinkType.COMPOSITION, ownedPath);
} else if (ownedPath.length() == 1 && ownedPath.isSingleField() && privateFields.contains(ownedPath.getSingleField(0).getName())) {
throw new IllegalStateException("getPrivateFields() contains " + ownedPath + " which points to an element which isn't private");
}
}
res.addAll(LinkType.ASSOCIATION, ownedPaths);
return res;
}
final List<ReferenceAction> getPossibleActions(final LinkType lt, final SQLElement targetElem) {
// MAYBE move required fields to SQLElement and use RESTRICT
final List<ReferenceAction> res;
if (lt == LinkType.PARENT) {
// SET_EMPTY would create an orphan
res = Arrays.asList(ReferenceAction.CASCADE, ReferenceAction.RESTRICT);
} else if (lt == LinkType.COMPOSITION) {
res = Arrays.asList(ReferenceAction.SET_EMPTY, ReferenceAction.RESTRICT);
} else {
assert lt == LinkType.ASSOCIATION;
if (targetElem.isShared()) {
res = Arrays.asList(ReferenceAction.RESTRICT, ReferenceAction.SET_EMPTY);
} else {
res = Arrays.asList(ReferenceAction.values());
}
}
return res;
}
private synchronized void initFF() {
if (areRelationshipsInited())
return;
final SQLElementLinksSetup paths = new SQLElementLinksSetup(this);
setupLinks(paths);
this.ownedLinks = new SQLElementLinks(paths.getResult());
// try to fill old attributes
final SQLElementLink parentLink = this.getParentLink();
if (parentLink != null) {
if (parentLink.getSingleField() != null)
this.parentFF = parentLink.getSingleField().getName();
else
throw new UnsupportedOperationException("Parent field name not supported : " + parentLink);
} else {
this.parentFF = null;
}
assert assertPrivateDefaultValues();
// if we added links, let the owned know
final Set<SQLElement> toReset = new HashSet<SQLElement>();
for (final SQLElementLink l : this.ownedLinks.getByPath().values()) {
toReset.add(l.getOwned());
}
for (final SQLElement e : toReset) {
e.resetRelationshipsOf(this);
}
this.ffInited();
}
// since by definition private cannot be shared, the default value must be empty
private final boolean assertPrivateDefaultValues() {
final Set<SQLElementLink> privates = this.getOwnedLinks().getByType(LinkType.COMPOSITION);
for (final SQLElementLink e : privates) {
if (!e.isJoin()) {
final SQLField singleField = e.getSingleField();
final Number privateDefault = (Number) singleField.getParsedDefaultValue().getValue();
final Number foreignUndef = e.getPath().getLast().getUndefinedIDNumber();
assert NumberUtils.areNumericallyEqual(privateDefault, foreignUndef) : singleField + " not empty : " + privateDefault;
}
}
return true;
}
public boolean isPrivate() {
return false;
}
/**
* Set {@link LinkType type} and other information for each owned link of this element.
*
* @param links the setup object.
*/
protected void setupLinks(SQLElementLinksSetup links) {
}
/**
* Was used to set the action of an {@link SQLElementLink}.
*
* @deprecated use {@link SQLElementLinkSetup#setType(LinkType, ReferenceAction)}
*/
protected void ffInited() {
// MAYBE use DELETE_RULE of Link
}
private final Set<SQLField> getSingleFields(final SQLElementLinks links, final LinkType type) {
final Set<SQLField> res = new HashSet<SQLField>();
for (final SQLElementLink l : links.getByType(type)) {
final SQLField singleField = l.getSingleField();
if (singleField == null)
throw new IllegalStateException("Not single field : " + l);
res.add(singleField);
}
return res;
}
private synchronized void initRF() {
if (this.otherLinks != null)
return;
final Set<Path> otherPaths = this.createPaths(false);
if (otherPaths.isEmpty()) {
this.otherLinks = SQLElementLinks.empty();
} else {
final SetMap<LinkType, SQLElementLink> tmp = new SetMap<LinkType, SQLElementLink>();
for (final Path p : otherPaths) {
final SQLElement refElem = this.getElementLenient(p.getFirst());
final SQLElementLink elementLink;
if (refElem == null) {
// RESTRICT : play it safe
elementLink = new SQLElementLink(null, p, this, LinkType.ASSOCIATION, null, ReferenceAction.RESTRICT);
} else {
elementLink = refElem.getOwnedLinks().getByPath(p);
assert elementLink.getOwned() == this;
}
tmp.add(elementLink.getLinkType(), elementLink);
}
this.otherLinks = new SQLElementLinks(tmp);
}
}
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));
}
/**
* 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.defaultName = 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.defaultName;
}
/**
* 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 SQLFieldTranslator trns = dir == null ? null : dir.getTranslator();
final Phrase res = trns == null ? null : trns.getElementName(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 ListMap<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 synchronized final SQLCache<SQLRow, Object> getModelCache() {
if (this.modelCache == null)
this.modelCache = new SQLCache<SQLRow, Object>(60, -1, "modelObjects of " + this.getCode());
return this.modelCache;
}
// *** 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) {
return this.update(from, to, false);
}
public final UpdateScript update(final SQLRowValues from, final SQLRowValues to, final boolean allowedToChangeTo) {
return this.update(from, to, allowedToChangeTo, Transformer.<SQLRowValues> nopTransformer());
}
private final UpdateScript update(final SQLRowValues from, SQLRowValues to, boolean allowedToChangeTo, ITransformer<SQLRowValues, SQLRowValues> copy2originalRows) {
check(from);
check(to);
for (final SQLRowValues v : from.getGraph().getItems()) {
if (!v.hasID())
throw new IllegalArgumentException("missing id in " + v + " : " + from.printGraph());
}
if (!to.hasID()) {
if (!allowedToChangeTo) {
final Map<SQLRowValues, SQLRowValues> copied = to.getGraph().deepCopy(false);
to = copied.get(to);
allowedToChangeTo = true;
copy2originalRows = Transformer.fromMap(CollectionUtils.invertMap(new IdentityHashMap<SQLRowValues, SQLRowValues>(), copied));
}
// from already exists in the DB, so if we're re-using it for another row, all
// non-provided fields must be reset
to.fillWith(SQLRowValues.SQL_DEFAULT, false);
to.setPrimaryKey(from);
}
if (from.getID() != to.getID())
throw new IllegalArgumentException("not the same row: " + from + " != " + to);
final UpdateScript res = new UpdateScript(this, from, copy2originalRows.transformChecked(to));
// local values and foreign links
for (final FieldGroup group : to.getFieldGroups()) {
if (group.getKeyType() != Type.FOREIGN_KEY) {
// i.e. primary key or normal field
res.getUpdateRow().putAll(to.getAbsolutelyAll(), group.getFields());
} else {
final SQLKey k = group.getKey();
if (k.getFields().size() > 1)
throw new IllegalStateException("Multi-field not supported : " + k);
final String field = group.getSingleField();
assert field != null;
final Path p = new PathBuilder(getTable()).add(k.getForeignLink(), Direction.FOREIGN).build();
final SQLElementLink elemLink = this.getOwnedLinks().getByPath(p);
if (elemLink.getLinkType() == LinkType.COMPOSITION) {
final SQLElement privateElem = elemLink.getOwned();
final Object fromPrivate = from.getObject(field);
final Object toPrivate = to.getObject(field);
assert !from.isDefault(field) : "A row in the DB cannot have DEFAULT";
final boolean fromIsEmpty = from.isForeignEmpty(field);
// as checked in initFF() the default for a private is empty
final boolean toIsEmpty = to.isDefault(field) || to.isForeignEmpty(field);
if (fromIsEmpty && toIsEmpty) {
// nothing to do, don't add to v
} else if (fromIsEmpty) {
final SQLRowValues toPR = (SQLRowValues) toPrivate;
// 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)
assert CollectionUtils.getSole(toPR.getReferentRows(elemLink.getSingleField())) == to : "Shared private " + toPR.printGraph();
final SQLRowValues copy = toPR.deepCopy().removeReferents(elemLink.getSingleField());
res.getUpdateRow().put(field, copy);
res.mapRow(copy2originalRows.transformChecked(toPR), copy);
} else if (toIsEmpty) {
// cut and archive
res.getUpdateRow().putEmptyLink(field);
res.addToArchive(privateElem, from.getForeign(field));
} else {
// neither is empty
final Number fromForeignID = from.getForeignIDNumber(field);
if (fromForeignID == null)
throw new IllegalArgumentException("Non-empty private in old row, but null ID for " + elemLink);
if (toPrivate == null)
throw new IllegalArgumentException("Non-empty private in new row, but null value for " + elemLink);
assert toPrivate instanceof Number || toPrivate instanceof SQLRowValues;
// with the above check, toForeignID is null if and only if toPrivate is an
// SQLRowValues without ID
final Number toForeignID = to.getForeignIDNumber(field);
// if it's desired in the future, don't forget to only re-use ID if there's
// no reference to this private
if (toForeignID != null && !NumberUtils.areNumericallyEqual(fromForeignID, toForeignID))
throw new IllegalArgumentException("private have changed for " + field + " : " + fromPrivate + " != " + toPrivate);
if (toPrivate instanceof SQLRowValues) {
if (!(fromPrivate instanceof SQLRowValues))
throw new IllegalArgumentException("Asymetric graph, old row doesn't contain a row for " + elemLink + " : " + fromPrivate);
final SQLRowValues fromPR = (SQLRowValues) fromPrivate;
final SQLRowValues toPR = (SQLRowValues) toPrivate;
// must have same ID
res.put(field, privateElem.update(fromPR, toPR, allowedToChangeTo, copy2originalRows));
} else {
// if toPrivate is just an ID and the same as fromPrivate, nothing to do
assert toPrivate instanceof Number && toForeignID != null;
}
}
} else if (to.isDefault(field)) {
res.getUpdateRow().putDefault(field);
} else {
res.getUpdateRow().put(field, to.getForeignIDNumber(field));
}
}
}
// now owned referents
for (final SQLElementLink elemLink : this.getOwnedLinks().getByPath().values()) {
if (elemLink.isJoin()) {
final Path pathToFK = elemLink.getPath().minusLast();
final Set<String> joinTableLocalContentFields = pathToFK.getLast().getFieldsNames(VirtualFields.LOCAL_CONTENT);
if (elemLink.getLinkType() == LinkType.COMPOSITION) {
final Tuple2<List<SQLRowValues>, Map<Number, SQLRowValues>> fromPrivatesTuple = indexRows(from.followPath(elemLink.getPath(), CreateMode.CREATE_NONE, false));
// already checked at the start of the method
assert fromPrivatesTuple.get0().isEmpty() : "Existing rows without ID : " + fromPrivatesTuple.get0();
final Map<Number, SQLRowValues> fromPrivates = fromPrivatesTuple.get1();
BigDecimal minOrder = null, maxOrder = null;
for (final SQLRowValues fromJoin : from.followPath(pathToFK, CreateMode.CREATE_NONE, false)) {
final BigDecimal order = fromJoin.getOrder();
assert order != null;
if (minOrder == null || minOrder.compareTo(order) > 0)
minOrder = order;
if (maxOrder == null || maxOrder.compareTo(order) < 0)
maxOrder = order;
}
final Collection<SQLRowValues> toValues = to.followPath(elemLink.getPath(), CreateMode.CREATE_NONE, false);
final Tuple2<List<SQLRowValues>, Map<Number, SQLRowValues>> toPrivatesTuple = indexRows(toValues);
final Map<Number, SQLRowValues> toPrivates = toPrivatesTuple.get1();
/*
* Order of joined rows are dictated by their join rows. To avoid needing
* knowledge from the entire table, join orders are unique only among their
* owner.
*/
/*
* Since almost no DB has deferrable constraints, use non-overlapping order : if
* there's space before minOrder, then use it, otherwise use space after
* maxOrder.
*/
BigDecimal toOrder;
if (minOrder == null || BigDecimal.valueOf(toValues.size()).compareTo(minOrder) < 0) {
toOrder = BigDecimal.ONE;
} else {
assert maxOrder != null : "Minimum order isn't null but maximum order is";
toOrder = maxOrder.add(BigDecimal.ONE);
}
final Map<SQLRowValues, BigDecimal> toPrivatesOrder = new IdentityHashMap<>();
for (final SQLRowValues toVals : toValues) {
toPrivatesOrder.put(toVals, toOrder);
toOrder = toOrder.add(BigDecimal.ONE);
}
final List<Number> onlyInFrom = new ArrayList<Number>(fromPrivates.keySet());
onlyInFrom.removeAll(toPrivates.keySet());
final Set<Number> onlyInTo = new HashSet<Number>(toPrivates.keySet());
onlyInTo.removeAll(fromPrivates.keySet());
final Set<Number> inFromAndTo = new HashSet<Number>(toPrivates.keySet());
inFromAndTo.retainAll(fromPrivates.keySet());
if (!onlyInTo.isEmpty())
throw new IllegalStateException("Unknown IDs : " + onlyInTo + " for " + elemLink + " from IDs : " + fromPrivates);
// pair of rows (old row then new row) with the same ID or with the new row
// lacking and ID
final List<SQLRowValues> matchedPrivates = new ArrayList<SQLRowValues>();
for (final Number inBoth : inFromAndTo) {
matchedPrivates.add(fromPrivates.get(inBoth));
matchedPrivates.add(toPrivates.get(inBoth));
}
final SQLElement privateElem = elemLink.getOwned();
final boolean hasReferences = !privateElem.getLinksOwnedByOthers().getByType(LinkType.ASSOCIATION).isEmpty();
final SQLField toMainField = elemLink.getPath().getStep(0).getSingleField();
final SQLField toPrivateField = elemLink.getPath().getStep(-1).getSingleField();
for (final SQLRowValues privateSansID : toPrivatesTuple.get0()) {
// don't re-use existing ID if there can be rows referencing it
if (!hasReferences && !onlyInFrom.isEmpty()) {
matchedPrivates.add(fromPrivates.get(onlyInFrom.remove(0)));
matchedPrivates.add(privateSansID);
} else {
// insert new, always creating the join row
final SQLRowValues copy = privateSansID.deepCopy().removeReferents(toPrivateField);
res.getUpdateRow().put(elemLink.getPath(), true, copy);
final SQLRowValues toJoinRow = CollectionUtils.getSole(privateSansID.getReferentRows(toPrivateField));
final SQLRowValues joinRow = CollectionUtils.getSole(copy.getReferentRows(toPrivateField));
setContentFields(joinTableLocalContentFields, joinRow, toJoinRow);
setOrder(joinRow, toPrivatesOrder.get(privateSansID));
res.mapRow(copy2originalRows.transformChecked(privateSansID), copy);
}
}
final Iterator<SQLRowValues> iter = matchedPrivates.iterator();
while (iter.hasNext()) {
final SQLRowValues fromPrivate = iter.next();
final SQLRowValues toPrivate = iter.next();
final SQLRowValues fromJoin = CollectionUtils.getSole(fromPrivate.getReferentRows(toPrivateField));
if (fromJoin == null)
throw new IllegalStateException("Shared private " + fromPrivate.printGraph());
final SQLRowValues toJoin = CollectionUtils.getSole(toPrivate.getReferentRows(toPrivateField));
final UpdateScript updateScript = privateElem.update(fromPrivate, toPrivate, allowedToChangeTo, copy2originalRows);
final SQLRowValues joinCopy = new SQLRowValues(fromJoin.getTable());
joinCopy.setID(fromJoin.getIDNumber());
assert joinCopy.getGraphSize() == 1;
setContentFields(joinTableLocalContentFields, joinCopy, toJoin);
setOrder(joinCopy, toPrivatesOrder.get(toPrivate));
joinCopy.put(toMainField.getName(), res.getUpdateRow());
joinCopy.put(toPrivateField.getName(), updateScript.getUpdateRow());
res.add(updateScript);
}
for (final Number id : onlyInFrom) {
// this will also cut the link from the main row
res.addToArchive(privateElem, fromPrivates.get(id));
}
} else {
final Step fkStep = elemLink.getPath().getStep(-1);
final String fkField = fkStep.getSingleField().getName();
final List<SQLRowValues> fromFKs = new ArrayList<SQLRowValues>(from.followPath(pathToFK, CreateMode.CREATE_NONE, false));
final List<SQLRowValues> toFKs = new ArrayList<SQLRowValues>(to.followPath(pathToFK, CreateMode.CREATE_NONE, false));
for (final SQLRowValues rowWithFK : toFKs) {
final int ownedID = rowWithFK.getForeignID(fkField);
final SQLRowValues toUse;
if (fromFKs.isEmpty()) {
toUse = res.getUpdateRow().putRowValues(pathToFK, true);
} else {
// take first available join
final SQLRowValues fromJoin = fromFKs.remove(0);
// if its values are what is needed don't update the DB (don't try to
// compare local field values)
if (ownedID == fromJoin.getForeignID(fkField) && joinTableLocalContentFields.isEmpty()) {
toUse = null;
} else {
// copy existing join ID to avoid inserting a new join in the DB
toUse = new SQLRowValues(fromJoin.getTable()).setID(fromJoin.getIDNumber());
res.getUpdateRow().put(elemLink.getPath().getStep(0), toUse);
}
}
if (toUse != null) {
setContentFields(joinTableLocalContentFields, toUse, rowWithFK);
toUse.put(fkField, ownedID);
res.mapRow(copy2originalRows.transformChecked(rowWithFK), toUse);
}
}
// lastly, delete remaining join rows (don't just archive otherwise if the main
// row is unarchived it will get back all links from every modification)
for (final SQLRowValues rowWithFK : fromFKs) {
res.addToDelete(rowWithFK);
}
}
} // else foreign link already handled above
}
return res;
}
// first rows without IDs, then those with IDs
static private Tuple2<List<SQLRowValues>, Map<Number, SQLRowValues>> indexRows(final Collection<SQLRowValues> rows) {
final List<SQLRowValues> sansID = new ArrayList<SQLRowValues>();
final Map<Number, SQLRowValues> map = new HashMap<Number, SQLRowValues>();
for (final SQLRowValues r : rows) {
if (r.hasID()) {
final SQLRowValues previous = map.put(r.getIDNumber(), r);
if (previous != null)
throw new IllegalStateException("Duplicate " + r.asRow());
} else {
sansID.add(r);
}
}
return Tuple2.create(sansID, map);
}
static private void setOrder(final SQLRowValues row, final BigDecimal order) {
assert order != null;
row.put(row.getTable().getOrderField().getName(), order);
}
static private void setContentFields(final Set<String> joinTableLocalContentFields, final SQLRowValues newJoinRow, final SQLRowValues rowToStore) {
if (!joinTableLocalContentFields.isEmpty()) {
// copy passed LOCAL_CONTENT fields (e.g. label)
newJoinRow.putAll(rowToStore.getValues(joinTableLocalContentFields));
// reset those not passed
if (newJoinRow.hasID())
newJoinRow.fill(joinTableLocalContentFields, SQLRowValues.SQL_DEFAULT, false, true);
}
}
public final void unarchiveNonRec(int id) throws SQLException {
this.unarchive(this.getTable().getRow(id), false);
}
public final void unarchive(int id) throws SQLException {
this.unarchive(this.getTable().getRow(id));
}
public final void unarchive(final SQLRow row) throws SQLException {
this.unarchive(row, true);
}
public void unarchive(final SQLRow row, final boolean desc) throws SQLException {
checkUndefined(row);
// don't test row.isArchived() (it is done by getTree())
// to allow an unarchived parent to unarchive all its descendants.
// make sure that all fields are loaded
final SQLRow upToDate = row.getTable().getRow(row.getID());
// nos descendants
final SQLRowValues descsAndMe = desc ? this.getTree(upToDate, true) : upToDate.asRowValues();
final SQLRowValues connectedRows = new ArchivedGraph(this.getDirectory(), descsAndMe).expand();
SQLUtils.executeAtomic(this.getTable().getBase().getDataSource(), new SQLFactory<Object>() {
@Override
public Object create() throws SQLException {
setArchive(Collections.singletonList(connectedRows.getGraph()), false);
return null;
}
});
}
public final void archive(int id) throws SQLException {
this.archiveIDs(Collections.singleton(id));
}
public final void archiveIDs(final Collection<? extends Number> ids) throws SQLException {
this.archive(TreesOfSQLRows.createFromIDs(this, ids), true);
}
public final void archive(final Collection<? extends SQLRowAccessor> rows) throws SQLException {
// rows checked by TreesOfSQLRows
this.archive(new TreesOfSQLRows(this, rows), true);
}
public final void archive(SQLRow row) throws SQLException {
this.archive(row, true);
}
/**
* Archive la ligne demandée et tous ses descendants mais ne cherche pas à couper les références
* 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, accélère grandement (par exemple pour
* OBSERVATION) car pas besoin de chercher toutes les références.
*
* @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());
if ((trees.isFetched() ? trees.getTrees().keySet() : trees.getRows()).isEmpty())
return;
for (final SQLRow row : trees.getRows())
checkUndefined(row);
SQLUtils.executeAtomic(this.getTable().getBase().getDataSource(), new SQLFactory<Object>() {
@Override
public Object create() throws SQLException {
if (!trees.isFetched())
trees.fetch(LockStrength.UPDATE);
// reference
// d'abord couper les liens qui pointent sur les futurs archivés
if (cutLinks) {
// TODO prend bcp de temps
// FIXME update tableau pour chaque observation, ecrase les changements
// faire : 'La base à changée voulez vous recharger ou garder vos modifs ?'
final Map<SQLElementLink, ? extends Collection<SQLRowValues>> externReferences = trees.getExternReferences().getMap();
// avoid toString() which might make requests to display rows (eg archived)
if (Log.get().isLoggable(Level.FINEST))
Log.get().finest("will cut : " + externReferences);
for (final Entry<SQLElementLink, ? extends Collection<SQLRowValues>> e : externReferences.entrySet()) {
final SQLElementLink linkToCut = e.getKey();
try {
if (linkToCut.isJoin()) {
final Path joinPath = linkToCut.getPath();
final Path toJoinTable = joinPath.minusLast();
final SQLTable joinTable = toJoinTable.getLast();
assert getElement(joinTable) instanceof JoinSQLElement;
final Set<Number> ids = new HashSet<Number>();
for (final SQLRowValues joinRow : e.getValue()) {
assert joinRow.getTable() == joinTable;
ids.add(joinRow.getIDNumber());
}
// MAYBE instead of losing the information (as with simple foreign
// key), archive it
final String query = "DELETE FROM " + joinTable.getSQLName() + " WHERE " + new Where(joinTable.getKey(), ids);
getTable().getDBSystemRoot().getDataSource().execute(query);
for (final Number id : ids)
joinTable.fireRowDeleted(id.intValue());
} else {
final Link refKey = linkToCut.getSingleLink();
for (final SQLRowAccessor ref : e.getValue()) {
ref.createEmptyUpdateRow().putEmptyLink(refKey.getSingleField().getName()).update();
}
}
} catch (Exception e1) {
throw new SQLException("Couldn't cut " + linkToCut + " in " + trees, e1);
}
}
Log.get().finest("done cutting links");
}
// on archive tous nos descendants
setArchive(trees.getClusters(), true);
return null;
}
});
}
static private final SQLRowValues setArchive(SQLRowValues r, final boolean archive) throws SQLException {
final SQLField archiveField = r.getTable().getArchiveField();
final Object newVal;
if (Boolean.class.equals(archiveField.getType().getJavaType()))
newVal = archive;
else
newVal = archive ? 1 : 0;
r.put(archiveField.getName(), newVal);
return r;
}
// all rows will be either archived or unarchived (handling cycles)
static private void setArchive(final Collection<SQLRowValuesCluster> clustersToArchive, final boolean archive) throws SQLException {
final Set<SQLRowValues> toArchive = Collections.newSetFromMap(new IdentityHashMap<SQLRowValues, Boolean>());
for (final SQLRowValuesCluster c : clustersToArchive)
toArchive.addAll(c.getItems());
final Map<SQLRow, SQLRowValues> linksCut = new HashMap<SQLRow, SQLRowValues>();
while (!toArchive.isEmpty()) {
// archive the maximum without referents
// or unarchive the maximum without foreigns
int archivedCount = -1;
while (archivedCount != 0) {
archivedCount = 0;
final Iterator<SQLRowValues> iter = toArchive.iterator();
while (iter.hasNext()) {
final SQLRowValues desc = iter.next();
final boolean correct;
if (desc.isArchived() == archive) {
// all already correct rows should be removed in the first loop, so they
// cannot be in linksCut
assert !linksCut.containsKey(desc.asRow());
correct = true;
} else if (archive && !desc.hasReferents() || !archive && !desc.hasForeigns()) {
SQLRowValues updateVals = linksCut.remove(desc.asRow());
if (updateVals == null)
updateVals = new SQLRowValues(desc.getTable());
// ne pas faire les fire après sinon qd on efface plusieurs éléments
// de la même 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
setArchive(updateVals, archive).setID(desc.getIDNumber());
// don't check validity since table events might have not already be
// fired
assert updateVals.getGraphSize() == 1 : "Archiving a graph : " + updateVals.printGraph();
updateVals.getGraph().store(StoreMode.COMMIT, false);
correct = true;
} else {
correct = false;
}
if (correct) {
// remove from graph
desc.clear();
desc.clearReferents();
assert desc.getGraphSize() == 1 : "Next loop won't progress : " + desc.printGraph();
archivedCount++;
iter.remove();
}
}
}
// if not empty there's at least one cycle
if (!toArchive.isEmpty()) {
// Identify one cycle, ATTN first might not be itself part of the cycle, like the
// BATIMENT and the LOCALs :
/**
* <pre>
* BATIMENT
* | \
* LOCAL1 LOCAL2
* | \
* CPI ---> SOURCE
* <--/
* </pre>
*/
final SQLRowValues first = toArchive.iterator().next();
// Among the rows in the cycle, archive one by cutting links (choose
// one with the least of them)
final AtomicReference<SQLRowValues> cutLinksRef = new AtomicReference<SQLRowValues>(null);
first.getGraph().walk(first, null, new ITransformer<State<Object>, Object>() {
@Override
public Object transformChecked(State<Object> input) {
final SQLRowValues last = input.getCurrent();
boolean cycleFound = false;
int minLinksCount = -1;
SQLRowValues leastLinks = null;
final Iterator<SQLRowValues> iter = input.getValsPath().iterator();
while (iter.hasNext()) {
final SQLRowValues v = iter.next();
if (!cycleFound) {
// start of cycle found
cycleFound = iter.hasNext() && v == last;
}
if (cycleFound) {
// don't use getReferentRows() as it's not the row count but
// the link count that's important
final int linksCount = archive ? v.getReferentsMap().allValues().size() : v.getForeigns().size();
// otherwise should have been removed above
assert linksCount > 0;
if (leastLinks == null || linksCount < minLinksCount) {
leastLinks = v;
minLinksCount = linksCount;
}
}
}
if (cycleFound) {
cutLinksRef.set(leastLinks);
throw new StopRecurseException();
}
return null;
}
}, new WalkOptions(Direction.REFERENT).setRecursionType(RecursionType.BREADTH_FIRST).setStartIncluded(false).setCycleAllowed(true));
final SQLRowValues cutLinks = cutLinksRef.get();
// if there were no cycles rows would have been removed above
assert cutLinks != null;
// cut links, and store them to be restored
if (archive) {
for (final Entry<SQLField, Set<SQLRowValues>> e : new SetMap<SQLField, SQLRowValues>(cutLinks.getReferentsMap()).entrySet()) {
final String fieldName = e.getKey().getName();
for (final SQLRowValues v : e.getValue()) {
// store before cutting
SQLRowValues cutVals = linksCut.get(v.asRow());
if (cutVals == null) {
cutVals = new SQLRowValues(v.getTable());
linksCut.put(v.asRow(), cutVals);
}
assert !cutVals.getFields().contains(fieldName) : fieldName + " already cut for " + v;
assert !v.isForeignEmpty(fieldName) : "Nothing to cut";
cutVals.put(fieldName, v.getForeignIDNumber(fieldName));
// cut graph
v.putEmptyLink(fieldName);
// cut DB
new SQLRowValues(v.getTable()).putEmptyLink(fieldName).update(v.getID());
}
}
} else {
// store before cutting
final Set<String> foreigns = new HashSet<String>(cutLinks.getForeigns().keySet());
final SQLRowValues oldVal = linksCut.put(cutLinks.asRow(), new SQLRowValues(cutLinks, ForeignCopyMode.COPY_ID_OR_RM));
// can't pass twice, as the first time we clear all foreigns, so the next loop
// must unarchive it.
assert oldVal == null : "Already cut";
// cut graph
cutLinks.removeAll(foreigns);
// cut DB
final SQLRowValues updateVals = new SQLRowValues(cutLinks.getTable());
for (final String fieldName : foreigns) {
updateVals.putEmptyLink(fieldName);
}
updateVals.update(cutLinks.getID());
}
// ready to begin another loop
assert archive && !cutLinks.hasReferents() || !archive && !cutLinks.hasForeigns();
}
}
// for unarchive we need to update again the already treated (unarchived) row
assert !archive || linksCut.isEmpty() : "Some links weren't restored : " + linksCut;
if (!archive) {
for (final Entry<SQLRow, SQLRowValues> e : linksCut.entrySet()) {
e.getValue().update(e.getKey().getID());
}
}
}
public void delete(SQLRowAccessor r) throws SQLException {
this.check(r);
if (true)
throw new UnsupportedOperationException("not yet implemented.");
}
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 SQLElementLinks getLinksOwnedByOthers() {
this.initRF();
return this.otherLinks;
}
private final Set<SQLField> getReferentFields(final LinkType type) {
return getSingleFields(this.getLinksOwnedByOthers(), type);
}
// not deprecated since joins to parents are unsupported (and unecessary since an SQLElement can
// only have one parent)
public final Set<SQLField> getChildrenReferentFields() {
return this.getReferentFields(LinkType.PARENT);
}
// *** ff
public synchronized final SQLElementLinks getOwnedLinks() {
this.initFF();
return this.ownedLinks;
}
public final SQLElementLink getOwnedLink(final String fieldName) {
return this.getOwnedLink(fieldName, null);
}
/**
* Return the {@link #getOwnedLinks() owned link} that crosses the passed field.
*
* @param fieldName any field of {@link #getTable()}.
* @param type the type of the wanted link, <code>null</code> meaning any type.
* @return the link matching the parameter.
*/
public final SQLElementLink getOwnedLink(final String fieldName, final LinkType type) {
final Link foreignLink = this.getTable().getDBSystemRoot().getGraph().getForeignLink(this.getTable().getField(fieldName));
if (foreignLink == null)
return null;
return this.getOwnedLinks().getByPath(new PathBuilder(getTable()).add(foreignLink, Direction.FOREIGN).build(), type);
}
public final boolean hasOwnedLinks(final LinkType type) {
return !this.getOwnedLinks().getByType(type).isEmpty();
}
public final SQLField getParentForeignField() {
return getOptionalField(this.getParentForeignFieldName());
}
public final synchronized String getParentForeignFieldName() {
this.initFF();
return this.parentFF;
}
public final SQLElementLink getParentLink() {
return CollectionUtils.getSole(this.getOwnedLinks().getByType(LinkType.PARENT));
}
public final Set<SQLElementLink> getChildrenLinks() {
return this.getLinksOwnedByOthers().getByType(LinkType.PARENT);
}
public final SQLElement getChildElement(final String tableName) {
final Set<SQLElementLink> links = new HashSet<SQLElementLink>();
for (final SQLElementLink childLink : this.getChildrenLinks()) {
if (childLink.getOwner().getTable().getName().equals(tableName))
links.add(childLink);
}
if (links.size() != 1)
throw new IllegalStateException("no exactly one child table named " + tableName + " : " + links);
else
return links.iterator().next().getOwner();
}
// optional but if specified it must exist
private final SQLField getOptionalField(final String name) {
return name == null ? null : this.getTable().getField(name);
}
// Previously there was another method which listed children but 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).
/**
* Should be overloaded to specify our parent.
*
* @return <code>null</code> for this implementation.
*/
protected String getParentFFName() {
return null;
}
public final SQLElement getParentElement() {
if (this.getParentForeignFieldName() == null)
return null;
else
return this.getForeignElement(this.getParentForeignFieldName());
}
public final SQLElement getPrivateElement(String foreignField) {
final SQLElementLink privateLink = this.getOwnedLink(foreignField, LinkType.COMPOSITION);
return privateLink == null ? null : privateLink.getOwned();
}
/**
* The graph of this table and its privates.
*
* @return an SQLRowValues of this element's table filled with
* {@link SQLRowValues#setAllToNull() <code>null</code>s} except for private foreign
* fields containing SQLRowValues.
* @deprecated renamed to {@link #createGraph()} since there's also join tables and each call
* creates a new instance.
*/
public final SQLRowValues getPrivateGraph() {
return this.createGraph();
}
/**
* The graph of this table, its privates and join tables.
*
* @return a graph of SQLRowValues filled with <code>null</code>s.
*/
public final SQLRowValues createGraph() {
return this.createGraph(VirtualFields.ALL);
}
/**
* The graph of this table, its privates and join tables.
*
* @param fields which fields should be included in the graph, not <code>null</code>.
* @return a graph of SQLRowValues filled with <code>null</code>s according to the
* <code>fields</code> parameter.
*/
public final SQLRowValues createGraph(final VirtualFields fields) {
return this.createGraph(fields, PrivateMode.ALL_PRIVATES, true);
}
static public enum PrivateMode {
NO_PRIVATES, DEEP_COPIED_PRIVATES, ALL_PRIVATES;
}
static private final SQLRowValues putNulls(final SQLRowValues res, final VirtualFields fields) {
return res.fill(res.getTable().getFieldsNames(fields), null, false, true);
}
public final SQLRowValues createGraph(final VirtualFields fields, final PrivateMode privateMode, final boolean includeJoins) {
final SQLRowValues res = putNulls(new SQLRowValues(this.getTable()), fields);
if (includeJoins) {
for (final SQLElementLink link : this.getOwnedLinks().getByPath().values()) {
if (link.isJoin()) {
putNulls(res.putRowValues(link.getPath().getStep(0)), fields);
}
}
}
if (privateMode != PrivateMode.NO_PRIVATES) {
for (final SQLElementLink link : this.getOwnedLinks().getByType(LinkType.COMPOSITION)) {
final SQLElement owned = link.getOwned();
if (privateMode == PrivateMode.DEEP_COPIED_PRIVATES && owned.dontDeepCopy()) {
res.remove(link.getPath().getStep(0));
} else {
res.put(link.getPath(), false, owned.createGraph(fields, privateMode, includeJoins));
}
}
}
return res;
}
/**
* Renvoie les champs qui sont 'privé' càd que les ligne pointées par ce champ ne sont
* référencées 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 privés, eg ["ID_OBSERVATION_2"].
* @deprecated use {@link #setupLinks(SQLElementLinksSetup)}
*/
protected List<String> getPrivateFields() {
return Collections.emptyList();
}
public final void clearPrivateFields(SQLRowValues rowVals) {
for (SQLElementLink l : this.getOwnedLinks().getByType(LinkType.COMPOSITION)) {
rowVals.remove(l.getPath().getStep(0));
}
}
/**
* 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 {
final Path p = new PathBuilder(getTable()).addForeignField(ff).build();
this.getOwnedLinks().getByPath(p).setAction(action);
}
// *** rf and ff
/**
* The links towards the parents (either {@link LinkType#PARENT} or {@link LinkType#COMPOSITION}
* ) of this element.
*
* @return the links towards the parents of this element.
*/
public final SQLElementLinks getContainerLinks() {
return getContainerLinks(true, true);
}
public final SQLElementLinks getContainerLinks(final boolean privateParent, final boolean parent) {
final SetMapItf<LinkType, SQLElementLink> byType = new SetMap<LinkType, SQLElementLink>();
if (parent)
byType.addAll(LinkType.PARENT, this.getOwnedLinks().getByType(LinkType.PARENT));
if (privateParent)
byType.addAll(LinkType.COMPOSITION, this.getLinksOwnedByOthers().getByType(LinkType.COMPOSITION));
final SQLElementLinks res = new SQLElementLinks(byType);
assert res.getByType().size() <= 1 : "Child and private at the same time";
return res;
}
// *** 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();
}
}
public final ComboSQLRequest createComboRequest() {
return this.createComboRequest(null, null);
}
public final ComboSQLRequest createComboRequest(final List<String> fields, final Where w) {
final ComboSQLRequest res = new ComboSQLRequest(this.getTable(), fields == null ? this.getComboFields() : fields, w, this.getDirectory());
this._initComboRequest(res);
return res;
}
protected void _initComboRequest(final ComboSQLRequest req) {
}
// 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;
}
/**
* Return the field expander to pass to {@link ListSQLRequest}.
*
* @return the {@link FieldExpander} to pass to {@link ListSQLRequest}.
* @see #createListRequest(List, Where, FieldExpander)
*/
protected FieldExpander getListExpander() {
return getDirectory().getShowAs();
}
public final ListSQLRequest createListRequest() {
return this.createListRequest(null);
}
public final ListSQLRequest createListRequest(final List<String> fields) {
return this.createListRequest(fields, null, null);
}
/**
* Create and initialise a new list request with the passed arguments. Pass <code>null</code>
* for default arguments.
*
* @param fields the list fields, <code>null</code> meaning {@link #getListFields()}.
* @param w the where, can be <code>null</code>.
* @param expander the field expander, <code>null</code> meaning {@link #getListExpander()}.
* @return a new ready-to-use list request.
*/
public final ListSQLRequest createListRequest(final List<String> fields, final Where w, final FieldExpander expander) {
final ListSQLRequest res = instantiateListRequest(fields == null ? this.getListFields() : fields, w, expander == null ? this.getListExpander() : expander);
this._initListRequest(res);
return res;
}
/**
* Must just create a new instance without altering parameters. The parameters are passed by
* {@link #createListRequest(List, Where, FieldExpander)}, if you need to change default values
* overload the needed method. This method should only be used if one needs a subclass of
* {@link ListSQLRequest}.
*
* @param fields the list fields.
* @param w the where.
* @param expander the field expander.
* @return a new uninitialised list request.
*/
protected ListSQLRequest instantiateListRequest(final List<String> fields, final Where w, final FieldExpander expander) {
return new ListSQLRequest(this.getTable(), fields, w, expander);
}
/**
* Initialise a new instance. E.g. one can {@link ListSQLRequest#addToGraphToFetch(String...)
* add fields} to the fetcher.
*
* @param req the instance to initialise.
*/
protected void _initListRequest(final ListSQLRequest req) {
}
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 = createTableSource();
}
return this.tableSrc;
} else
return this.createTableSource();
}
public final SQLTableModelSourceOnline createTableSource() {
return createTableSource((Where) null);
}
public final SQLTableModelSourceOnline createTableSource(final List<String> fields) {
return createTableSourceOnline(createListRequest(fields));
}
public final SQLTableModelSourceOnline createTableSource(final Where w) {
return createTableSourceOnline(createListRequest(null, w, null));
}
public final SQLTableModelSourceOnline createTableSourceOnline(final ListSQLRequest req) {
return initTableSource(instantiateTableSourceOnline(req));
}
protected SQLTableModelSourceOnline instantiateTableSourceOnline(final ListSQLRequest req) {
return new SQLTableModelSourceOnline(req, this);
}
protected synchronized void _initTableSource(final SQLTableModelSource res) {
if (!this.additionalListCols.isEmpty())
res.getColumns().addAll(this.additionalListCols);
}
public final <S extends SQLTableModelSource> S initTableSource(final S res) {
return this.initTableSource(res, false);
}
public final synchronized <S extends SQLTableModelSource> S initTableSource(final S res, final boolean minimal) {
res.init();
// do init first since it can modify the columns
if (!minimal)
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;
}
public final SQLTableModelSourceOffline createTableSourceOffline() {
return createTableSourceOfflineWithWhere(null);
}
public final SQLTableModelSourceOffline createTableSourceOfflineWithWhere(final Where w) {
return createTableSourceOffline(createListRequest(null, w, null));
}
public final SQLTableModelSourceOffline createTableSourceOffline(final ListSQLRequest req) {
return initTableSource(instantiateTableSourceOffline(req));
}
protected SQLTableModelSourceOffline instantiateTableSourceOffline(final ListSQLRequest req) {
return new SQLTableModelSourceOffline(req, this);
}
/**
* 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 SQLElementLink childLink : this.getChildrenLinks()) {
if (deep || !childLink.getChild().dontDeepCopy()) {
final SQLField childField = childLink.getSingleField();
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);
}
protected final 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 {
return copyRecursive(row, false, parent, c);
}
/**
* Copy <code>row</code> and its children into <code>parent</code>.
*
* @param row which row to clone.
* @param full <code>true</code> if {@link #dontDeepCopy()} should be ignored, i.e. an exact
* copy will be made.
* @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 boolean full, final SQLRow parent, final IClosure<SQLRowValues> c) throws SQLException {
check(row);
if (row.isUndefined())
return row;
// current => new copy
// contains private and join rows otherwise we can't fix ASSOCIATION
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, full, parent, copies, c);
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, full, null, copies, c);
descCopy.put(joint.getName(), parentCopy);
}
}, full, false, false);
// ne pas descendre en deep
// private and parent relationships are already handled, now fix ASSOCIATION : the
// associations in the source hierarchy either point outside or inside the
// hierarchy, for the former the copy is correct. But for the latter, the copy still
// point to the source hierarchy when it should point to copy hierarchy.
forDescendantsDo(row, new ChildProcessor<SQLRow>() {
public void process(SQLRow parent, SQLField joint, SQLRow desc) throws SQLException {
for (final SQLElementLink link : getElement(desc.getTable()).getOwnedLinks().getByType(LinkType.ASSOCIATION)) {
final Path toRowWFK = link.getPath().minusLast();
final Step lastStep = link.getPath().getStep(-1);
for (final SQLRow rowWithFK : desc.getDistantRows(toRowWFK)) {
final SQLRow ref = rowWithFK.getForeignRow(lastStep.getSingleLink(), SQLRowMode.NO_CHECK);
// eg copy of SOURCE[12] is SOURCE[354]
final SQLRowValues refCopy = copies.get(ref);
if (refCopy != null) {
// CPI[1203]
final SQLRowValues rowWithFKCopy = copies.get(rowWithFK);
rowWithFKCopy.put(lastStep, refCopy);
}
}
}
}
}, full);
// 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, final boolean full, SQLRow parent, final Map<SQLRow, SQLRowValues> map, final IClosure<SQLRowValues> c) throws SQLException {
final SQLRowValues copiedVals = getElement(desc.getTable()).createCopy(desc, full, parent, null, map);
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 will only access the DB if
* necessary : when <code>row</code> is not an {@link SQLRowValues} and this element has
* {@link LinkType#COMPOSITION privates} or {@link SQLElementLink#isJoin() joins}. Otherwise 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>. If it's an {@link SQLRowValues} it will be used directly, otherwise
* {@link SQLRowAccessor#getIDNumber()} will be used (i.e. if the copy isn't to be linked
* to its parent, pass a {@link SQLRowAccessor#asRow() row}).
* @return a copy ready to be inserted, or <code>null</code> if <code>row</code> cannot be
* copied.
*/
public SQLRowValues createCopy(SQLRowAccessor row, SQLRowAccessor parent) {
return createCopy(row, false, parent);
}
public SQLRowValues createCopy(SQLRowAccessor row, final boolean full, SQLRowAccessor parent) {
return this.createCopy(row, full, parent, null, null);
}
public SQLRowValues createCopy(SQLRowAccessor row, final boolean full, SQLRowAccessor parent, final IdentityHashMap<SQLRowValues, SQLRowValues> valsMap, final Map<SQLRow, SQLRowValues> rowMap) {
// do NOT copy the undefined
if (row == null || row.isUndefined())
return null;
this.check(row);
final Set<SQLElementLink> privates = this.getOwnedLinks().getByType(LinkType.COMPOSITION);
final SQLRowValues privateGraph = this.createGraph(VirtualFields.ALL, !full ? PrivateMode.DEEP_COPIED_PRIVATES : PrivateMode.ALL_PRIVATES, true);
// Don't make one request per private, just fetch the whole graph at once
// further with joined privates an SQLRow cannot contain privates nor carry the lack of them
// (without joins a row lacking privates was passed with just an SQLRow with undefined
// foreign keys).
final SQLRowValues rowVals;
if (row instanceof SQLRowValues) {
rowVals = (SQLRowValues) row;
} else if (privateGraph.getGraphSize() == 1) {
rowVals = null;
} else {
final SQLRowValuesListFetcher fetcher = SQLRowValuesListFetcher.create(privateGraph);
fetcher.setSelID(row.getIDNumber());
rowVals = CollectionUtils.getSole(fetcher.fetch());
if (rowVals == null)
throw new IllegalStateException("Not exactly one row for " + row);
}
// Use just fetched values so that data is coherent.
final SQLRowAccessor upToDateRow = rowVals != null ? rowVals : row;
final SQLRowValues copy = new SQLRowValues(this.getTable());
this.loadAllSafe(copy, upToDateRow);
if (valsMap != null) {
if (rowVals == null)
throw new IllegalArgumentException("Cannot fill map since no SQLRowValues were provided");
valsMap.put(rowVals, copy);
}
if (rowMap != null) {
if (!upToDateRow.hasID())
throw new IllegalArgumentException("Cannot fill map since no SQLRow were provided");
rowMap.put(upToDateRow.asRow(), copy);
}
for (final SQLElementLink privateLink : privates) {
final SQLElement privateElement = privateLink.getOwned();
final boolean deepCopy = full || !privateElement.dontDeepCopy();
if (!privateLink.isJoin()) {
final String privateName = privateLink.getSingleField().getName();
if (deepCopy && !rowVals.isForeignEmpty(privateName)) {
final SQLRowValues foreign = checkPrivateLoaded(privateLink, rowVals.getForeign(privateName));
final SQLRowValues child = privateElement.createCopy(foreign, full, null, valsMap, rowMap);
copy.put(privateName, child);
// use upToDateRow instead of rowVals since the latter might be null if
// !full
} else if (upToDateRow.getFields().contains(privateName)) {
copy.putEmptyLink(privateName);
}
} else {
// join
assert privateLink.getPath().getStep(0).getDirection() == Direction.REFERENT;
if (deepCopy) {
copyJoin(rowVals, full, valsMap, rowMap, copy, privateLink);
} // else nothing to do since there's no fields in copy
}
}
for (final SQLElementLink association : this.getOwnedLinks().getByType(LinkType.ASSOCIATION)) {
if (association.isJoin()) {
copyJoin(rowVals, full, valsMap, rowMap, copy, association);
} // else fields already in copy
}
// si on a spécifié 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 instanceof SQLRowValues ? parent : parent.getIDNumber());
}
return copy;
}
private SQLRowValues checkPrivateLoaded(final SQLElementLink privateLink, final SQLRowAccessor foreign) {
assert privateLink.getLinkType() == LinkType.COMPOSITION && privateLink.getOwned().getTable() == foreign.getTable();
// otherwise the recursive call will fetch the missing data, which could be
// incoherent with rowVals
if (!(foreign instanceof SQLRowValues))
throw new IllegalStateException("Graph missing non-empty private for " + privateLink);
return (SQLRowValues) foreign;
}
private final void copyJoin(final SQLRowValues rowVals, final boolean full, final IdentityHashMap<SQLRowValues, SQLRowValues> valsMap, final Map<SQLRow, SQLRowValues> rowMap,
final SQLRowValues copy, final SQLElementLink link) {
assert link.isJoin();
final Step firstStep = link.getPath().getStep(0);
final SQLElement joinElem = getElement(firstStep.getTo());
final Step lastStep = link.getPath().getStep(-1);
for (final SQLRowValues joinToCopy : rowVals.followPath(link.getPath().minusLast(), CreateMode.CREATE_NONE, false)) {
final SQLRowValues joinCopy = new SQLRowValues(joinElem.getTable());
joinElem.loadAllSafe(joinCopy, joinToCopy, link.getLinkType() == LinkType.COMPOSITION);
copy.put(firstStep, joinCopy);
if (valsMap != null)
valsMap.put(joinToCopy, joinCopy);
if (rowMap != null)
rowMap.put(joinToCopy.asRow(), joinCopy);
// copy private
if (link.getLinkType() == LinkType.COMPOSITION) {
final SQLElement privateElement = link.getOwned();
final SQLRowAccessor privateRow = joinToCopy.getForeign(lastStep.getSingleLink());
if (privateRow.isUndefined())
throw new IllegalStateException("Joined to undefined " + link);
checkPrivateLoaded(link, privateRow);
final SQLRowValues privateCopy = privateElement.createCopy(privateRow, full, null, valsMap, rowMap);
joinCopy.put(lastStep, privateCopy);
}
assert !joinCopy.hasID() && joinCopy.getFields().containsAll(lastStep.getSingleLink().getCols());
}
}
static private final VirtualFields JOIN_SAFE_FIELDS = VirtualFields.ALL.difference(VirtualFields.PRIMARY_KEY, VirtualFields.ORDER);
static private final VirtualFields SAFE_FIELDS = JOIN_SAFE_FIELDS.difference(VirtualFields.FOREIGN_KEYS);
/**
* Load all values that can be safely copied (shared by multiple rows). This means all values
* except private, primary, and order.
*
* @param vals the row to modify.
* @param row the row to be loaded.
*/
public final void loadAllSafe(final SQLRowValues vals, final SQLRowAccessor row) {
this.loadAllSafe(vals, row, null);
}
private final void loadAllSafe(final SQLRowValues vals, final SQLRowAccessor row, final Boolean isPrivateJoinElement) {
check(vals);
check(row);
// JoinSQLElement has no links but we still want to copy metadata
if (this instanceof JoinSQLElement) {
if (isPrivateJoinElement == null)
throw new IllegalStateException("joins are not public");
assert this.getOwnedLinks().getByPath().size() == 0;
vals.setAll(row.getValues(JOIN_SAFE_FIELDS));
// remove links to owned if private join
final Path pathFromOwner = ((JoinSQLElement) this).getPathFromOwner();
assert pathFromOwner.length() == 2;
if (isPrivateJoinElement)
vals.remove(pathFromOwner.getStep(1));
} else {
if (isPrivateJoinElement != null)
throw new IllegalStateException("should a join : " + this);
// Don't copy foreign keys then remove privates (i.e. JOIN_SAFE_FIELDS), as this will
// copy ignored paths (see SQLElementLinkSetup.ignore()) and they might be privates
vals.setAll(row.getValues(SAFE_FIELDS));
for (final SQLElementLink l : this.getOwnedLinks().getByPath().values()) {
if (l.getLinkType() != LinkType.COMPOSITION && !l.isJoin()) {
vals.putAll(row.getValues(l.getSingleLink().getCols()));
}
}
}
}
// *** 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 ListMap<SQLTable, SQLRow> getDescendants(SQLRow row) {
check(row);
final ListMap<SQLTable, SQLRow> mm = new ListMap<SQLTable, SQLRow>();
try {
this.forDescendantsDo(row, new ChildProcessor<SQLRow>() {
public void process(SQLRow parent, SQLField joint, SQLRow child) throws SQLException {
mm.add(joint.getTable(), child);
}
}, true);
} catch (SQLException e) {
// never happen
e.printStackTrace();
}
return mm;
}
/**
* Returns the tree beneath the passed row.
*
* @param row the root of the desired tree.
* @param archived <code>true</code> if the returned rows should be archived.
* @return the asked tree.
*/
private SQLRowValues getTree(SQLRow row, boolean archived) {
check(row);
final SQLRowValues res = row.asRowValues();
try {
this.forDescendantsDo(res, new ChildProcessor<SQLRowValues>() {
public void process(SQLRowValues parent, SQLField joint, SQLRowValues desc) throws SQLException {
desc.put(joint.getName(), parent);
}
}, true, false, archived);
} catch (SQLException e) {
// never happen cause process don't throw it
e.printStackTrace();
}
return res;
}
/**
* Returns the children of the passed row.
*
* @param row a SQLRow.
* @return the children rows by SQLTable.
*/
public ListMap<SQLTable, SQLRow> getChildrenRows(SQLRow row) {
check(row);
// List to retain order
final ListMap<SQLTable, SQLRow> mm = new ListMap<SQLTable, SQLRow>();
try {
this.forChildrenDo(row, new ChildProcessor<SQLRow>() {
public void process(SQLRow parent, SQLField joint, SQLRow child) throws SQLException {
mm.add(child.getTable(), child);
}
}, true, false);
} catch (SQLException e) {
// never happen
e.printStackTrace();
}
// TODO return Map of SQLElement instead of SQLTable (this avoids the caller a call to
// getDirectory())
return mm;
}
public SQLRowValues getContainer(final SQLRowValues row) {
return this.getContainer(row, true, true);
}
public final SQLRowValues getContainer(final SQLRowValues row, final boolean privateParent, final boolean parent) {
check(row);
if (row.isUndefined() || !privateParent && !parent)
return null;
final List<SQLRowValues> parents = new ArrayList<SQLRowValues>();
for (final SQLElementLink l : this.getContainerLinks(privateParent, parent).getByPath().values()) {
parents.addAll(row.followPath(l.getPathToParent(), CreateMode.CREATE_NONE, true));
}
if (parents.size() > 1)
throw new IllegalStateException("More than one parent for " + row + " : " + parents);
return parents.size() == 0 ? null : parents.get(0);
}
@Deprecated
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)
@Deprecated
private SQLRow getForeignParent(SQLRow row, final SQLRowMode mode) {
check(row);
return this.getParentForeignFieldName() == null ? null : row.getForeignRow(this.getParentForeignFieldName(), mode);
}
public final SQLRowValues fetchPrivateParent(final SQLRowAccessor row, final boolean modifyParameter) {
return this.fetchPrivateParent(row, modifyParameter, ArchiveMode.UNARCHIVED);
}
/**
* Return the parent if any of the passed row. This method will access the DB.
*
* @param row the row.
* @param modifyParameter <code>true</code> if <code>row</code> can be linked to the result,
* <code>false</code> to link a new {@link SQLRowValues}.
* @param archiveMode the parent must match this mode.
* @return the matching parent linked to its child, <code>null</code> if <code>row</code>
* {@link SQLRowAccessor#isUndefined()}, if this isn't a private or if no parent exist.
* @throws IllegalStateException if <code>row</code> has more than one parent matching.
*/
public final SQLRowValues fetchPrivateParent(final SQLRowAccessor row, final boolean modifyParameter, final ArchiveMode archiveMode) {
return this.fetchContainer(row, modifyParameter, archiveMode, true, false);
}
public final SQLRowValues fetchContainer(final SQLRowAccessor row) {
return fetchContainer(row, ArchiveMode.UNARCHIVED);
}
public final SQLRowValues fetchContainer(final SQLRowAccessor row, final ArchiveMode archiveMode) {
return this.fetchContainer(row, false, archiveMode, true, true);
}
static private SQLField getToID(final Step s) {
return s.isForeign() ? s.getSingleField() : s.getTo().getKey();
}
public final SQLRowValues fetchContainer(final SQLRowAccessor row, final boolean modifyParameter, final ArchiveMode archiveMode, final boolean privateParent, final boolean parent) {
check(row);
if (row.isUndefined() || !privateParent && !parent)
return null;
final SQLSyntax syntax = SQLSyntax.get(getTable());
final List<SQLElementLink> parentLinks = new ArrayList<SQLElementLink>(this.getContainerLinks(privateParent, parent).getByPath().values());
if (parentLinks.isEmpty())
return null;
final ListIterator<SQLElementLink> listIter = parentLinks.listIterator();
final List<String> selects = new ArrayList<String>(parentLinks.size());
while (listIter.hasNext()) {
final SQLElementLink parentLink = listIter.next();
final SQLSelect sel = new SQLSelect(true);
sel.addSelect(getToID(parentLink.getStepToParent()), null, "parentID");
final SQLField joinPK = parentLink.getPath().getTable(1).getKey();
if (parentLink.isJoin()) {
sel.addSelect(joinPK, null, "joinID");
} else {
sel.addRawSelect(syntax.cast("NULL", joinPK.getType()), "joinID");
}
sel.addRawSelect(String.valueOf(listIter.previousIndex()), "fieldIndex");
sel.setArchivedPolicy(archiveMode);
sel.setWhere(new Where(getToID(parentLink.getStepToChild()), "=", row.getIDNumber()));
assert sel.getTableRefs().size() == 1 : "Non optimal query";
selects.add(sel.asString());
}
final List<?> parentIDs = getTable().getDBSystemRoot().getDataSource().executeA(CollectionUtils.join(selects, "\nUNION ALL "));
if (parentIDs.size() > 1)
throw new IllegalStateException("More than one parent for " + row + " : " + parentIDs);
else if (parentIDs.size() == 0)
// e.g. no UNARCHIVED parent of an ARCHIVED private
return null;
final Object[] idAndIndex = (Object[]) parentIDs.get(0);
final Number mainID = (Number) idAndIndex[0];
final Number joinID = (Number) idAndIndex[1];
final SQLElementLink parentLink = parentLinks.get(((Number) idAndIndex[2]).intValue());
final Path toChildPath = parentLink.getPathToChild();
final SQLRowValues res = new SQLRowValues(toChildPath.getTable(0)).setID(mainID);
final SQLRowValues rowWithFK;
if (parentLink.isJoin()) {
if (joinID == null)
throw new IllegalStateException("Missing join ID for " + parentLink);
final Step parentToJoin = toChildPath.getStep(0);
rowWithFK = res.putRowValues(parentToJoin).setID(joinID);
} else {
rowWithFK = res;
}
assert rowWithFK.hasID();
// first convert to SQLRow to avoid modifying the (graph of our) method parameter
rowWithFK.put(toChildPath.getStep(-1), (modifyParameter ? row : row.asRow()).asRowValues());
return res;
}
/**
* Return the main row if any of the passed row. This method will access the DB.
*
* @param row the row, if it's a {@link SQLRowValues} it will be linked to the result.
* @param archiveMode the parent must match this mode.
* @return the matching parent linked to its child, <code>null</code> if <code>row</code>
* {@link SQLRowAccessor#isUndefined()}, if this isn't a private or if no parent exist.
* @see #fetchPrivateParent(SQLRowAccessor, boolean, ArchiveMode)
*/
public final SQLRowValues fetchPrivateRoot(SQLRowAccessor row, final ArchiveMode archiveMode) {
SQLRowValues prev = null;
SQLRowValues res = fetchPrivateParent(row, true, archiveMode);
while (res != null) {
prev = res;
res = getElement(res.getTable()).fetchPrivateParent(res, true, archiveMode);
}
return prev;
}
Map<SQLField, List<SQLRow>> getNonChildrenReferents(SQLRow row) {
check(row);
final Map<SQLField, List<SQLRow>> mm = new HashMap<SQLField, List<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.put(refField, row.getReferentRows(refField));
}
return mm;
}
/**
* Fetch the whole {@link #createGraph() graph} for the passed ID and wrap it in the model
* object.
*
* @param id the ID to fetch.
* @return a model object.
*/
public final Object fetchModelObject(Number id) {
final SQLRowValues r = createModelFetcher().fetchOne(id, true);
if (r == null)
throw new IllegalStateException("Missing " + id + " for " + this);
return this.getModelObject(r);
}
protected final SQLRowValuesListFetcher createModelFetcher() {
return SQLRowValuesListFetcher.create(this.createGraph());
}
public final Map<Number, Object> fetchModelObjects(Collection<? extends Number> ids) {
return this.fetchModelObjects(ids, Object.class);
}
public final <T> Map<Number, T> fetchModelObjects(Collection<? extends Number> ids, final Class<T> clazz) {
final List<SQLRowValues> rows = createModelFetcher().fetch(new Where(getTable().getKey(), ids), true);
final Map<Number, T> res = new LinkedHashMap<Number, T>();
for (final SQLRowValues r : rows) {
res.put(r.getIDNumber(), clazz.cast(this.getModelObject(r)));
}
return res;
}
/**
* Returns a java object modeling the passed row. No access to the DB will be performed.
*
* @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 #canCreateModelObject()
*/
public final Object getModelObject(SQLRowAccessor row) {
check(row);
if (!canCreateModelObject())
return null;
final Object res;
// only SQLRow are cached (otherwise need another cache to not return model objects with
// SQLRowValues if passed SQLRow and vice-versa)
if (row instanceof SQLRow) {
final CacheResult<Object> cached = this.getModelCache().check(row.asRow(), Collections.singleton(row));
if (cached.getState() == CacheResult.State.INTERRUPTED)
throw new RTInterruptedException("interrupted while waiting for the cache");
else if (cached.getState() == CacheResult.State.VALID)
return cached.getRes();
try {
res = this.createModelObject(row);
this.getModelCache().put(cached, res);
} catch (RuntimeException exn) {
this.getModelCache().removeRunning(cached);
throw exn;
}
} else
res = this.createModelObject(row);
return res;
}
protected final Object createModelObject(SQLRowAccessor row) {
return this.getModelClass().cast(this._createModelObject(row));
}
protected Object _createModelObject(SQLRowAccessor row) {
Constructor<?> ctor = ReflectUtils.getMatchingConstructor(this.getModelClass(), row.getClass(), this.getClass());
if (ctor == null) {
// deprecated constructor
try {
ctor = this.getModelClass().getConstructor(new Class[] { SQLRowAccessor.class });
} catch (NoSuchMethodException e) {
throw new IllegalStateException(this + " found no public suitable constructor in " + this.getModelClass());
}
}
try {
return ctor.getParameterTypes().length == 2 ? ctor.newInstance(new Object[] { row, this }) : ctor.newInstance(new Object[] { row });
} catch (Exception e) {
throw ExceptionUtils.createExn(RuntimeException.class, "pb creating instance", e);
}
}
public boolean canCreateModelObject() {
return this.getModelClass() != null;
}
protected Class<?> getModelClass() {
return null;
}
// *** equals
public static final class EqualOptionBuilder {
private boolean ignoreNotDeepCopied, testNonShared, testParent, testMetadata;
public EqualOptionBuilder() {
this.ignoreNotDeepCopied = false;
this.testNonShared = false;
this.testParent = false;
this.testMetadata = false;
}
public boolean isIgnoreNotDeepCopied() {
return this.ignoreNotDeepCopied;
}
public EqualOptionBuilder setIgnoreNotDeepCopied(boolean ignoreNotDeepCopied) {
this.ignoreNotDeepCopied = ignoreNotDeepCopied;
return this;
}
public boolean isNonSharedTested() {
return this.testNonShared;
}
public EqualOptionBuilder setNonSharedTested(boolean testNonShared) {
this.testNonShared = testNonShared;
return this;
}
public boolean isParentTested() {
return this.testParent;
}
public EqualOptionBuilder setParentTested(boolean testParent) {
this.testParent = testParent;
return this;
}
public boolean isMetadataTested() {
return this.testMetadata;
}
public EqualOptionBuilder setMetadataTested(boolean testMetadata) {
this.testMetadata = testMetadata;
return this;
}
public EqualOption build() {
return new EqualOption(this.ignoreNotDeepCopied, this.testNonShared, this.testParent, this.testMetadata);
}
}
@Immutable
public static final class EqualOption {
static private final VirtualFields EQUALS_FIELDS = VirtualFields.CONTENT.union(VirtualFields.ARCHIVE);
static private final VirtualFields EQUALS_WITH_MD_FIELDS = EQUALS_FIELDS.union(VirtualFields.METADATA);
public static final EqualOption ALL = new EqualOption(false, true, true, true);
public static final EqualOption ALL_BUT_IGNORE_NOT_DEEP_COPIED = ALL.createBuilder().setIgnoreNotDeepCopied(true).build();
public static final EqualOption IGNORE_NOT_DEEP_COPIED = new EqualOptionBuilder().setIgnoreNotDeepCopied(true).build();
public static final EqualOption TEST_NOT_DEEP_COPIED = new EqualOptionBuilder().setIgnoreNotDeepCopied(false).build();
static final EqualOption fromIgnoreNotDeepCopied(final boolean ignoreNotDeepCopied) {
return ignoreNotDeepCopied ? IGNORE_NOT_DEEP_COPIED : TEST_NOT_DEEP_COPIED;
}
private final boolean ignoreNotDeepCopied, testNonShared, testParent;
private final VirtualFields fields;
protected EqualOption(final boolean ignoreNotDeepCopied, final boolean testNonShared, final boolean testParent, final boolean testMetadata) {
this.ignoreNotDeepCopied = ignoreNotDeepCopied;
this.testNonShared = testNonShared;
this.testParent = testParent;
this.fields = testMetadata ? EQUALS_WITH_MD_FIELDS : EQUALS_FIELDS;
}
public boolean isIgnoreNotDeepCopied() {
return this.ignoreNotDeepCopied;
}
public boolean isNonSharedTested() {
return this.testNonShared;
}
public boolean isParentTested() {
return this.testParent;
}
public EqualOptionBuilder createBuilder() {
return new EqualOptionBuilder().setIgnoreNotDeepCopied(isIgnoreNotDeepCopied()).setNonSharedTested(isNonSharedTested()).setParentTested(this.isParentTested())
.setMetadataTested(this.fields == EQUALS_WITH_MD_FIELDS);
}
}
public boolean equals(SQLRow row, SQLRow row2) {
return this.equals(row, row2, false);
}
/**
* Compare local values (excluding order and obviously primary key). This method doesn't cross
* links except for privates but it does compare the value of shared normal links. This method
* always uses the DB.
*
* @param row the first row.
* @param row2 the second row.
* @param ignoreNotDeepCopied if <code>true</code> ignores the rows that are
* {@link #dontDeepCopy() not to be copied}. See also the <code>full</code> parameter of
* {@link #createCopy(SQLRowAccessor, boolean, SQLRowAccessor)}.
* @return <code>true</code> if the two rows are equal.
* @see #equals(SQLRowValues, SQLRowValues, boolean)
*/
public boolean equals(SQLRow row, SQLRow row2, boolean ignoreNotDeepCopied) {
return this.equals(row, row2, EqualOption.fromIgnoreNotDeepCopied(ignoreNotDeepCopied));
}
public boolean equals(SQLRow row, SQLRow row2, final EqualOption option) {
return this.diff(row, row2, option).get0();
}
private static final Tuple2<Boolean, DiffResult> TRUE_NULL = new Tuple2<Boolean, DiffResult>(true, null);
private static final Tuple2<Boolean, DiffResult> FALSE_NULL = new Tuple2<Boolean, DiffResult>(false, null);
// Boolean is never null, DiffResult is null if difference is trivial
Tuple2<Boolean, DiffResult> diff(SQLRow row, SQLRow row2, final EqualOption option) {
check(row);
if (!row2.getTable().equals(this.getTable()))
return FALSE_NULL;
if (row.equals(row2))
return TRUE_NULL;
// the same table but not the same id
final SQLRowValuesListFetcher fetcher = SQLRowValuesListFetcher.create(getPrivateGraphForEquals(option));
final List<SQLRowValues> fetched = fetcher.fetch(new Where(this.getTable().getKey(), Arrays.asList(row.getIDNumber(), row2.getIDNumber())));
if (fetched.size() > 2)
throw new IllegalStateException("More than 2 rows for " + row + " and " + row2);
else if (fetched.size() < 2)
// at least one is inexistent or archived
return FALSE_NULL;
final DiffResult res = equalsPruned(fetched.get(0), fetched.get(1));
return Tuple2.create(res.isEqual(), res);
}
/**
* Compare local values (excluding order and obviously primary key). This method doesn't cross
* links except for privates but it does compare the value of shared normal links. This method
* never uses the DB but does {@link SQLRowValuesCluster#prune(SQLRowValues, SQLRowValues)
* prune} the parameters before comparing them.
*
* @param row the first row.
* @param row2 the second row.
* @param ignoreNotDeepCopied if <code>true</code> ignores the rows that are
* {@link #dontDeepCopy() not to be copied}. See also the <code>full</code> parameter of
* {@link #createCopy(SQLRowAccessor, boolean, SQLRowAccessor)}.
* @return <code>true</code> if the two rows are equal.
* @see #equals(SQLRow, SQLRow, boolean)
*/
public boolean equals(SQLRowValues row, SQLRowValues row2, boolean ignoreNotDeepCopied) {
return this.equals(row, row2, EqualOption.fromIgnoreNotDeepCopied(ignoreNotDeepCopied));
}
public boolean equals(SQLRowValues row, SQLRowValues row2, final EqualOption option) {
check(row);
if (row == row2)
return true;
if (!row2.getTable().equals(this.getTable()))
return false;
final SQLRowValues privateGraphForEquals = getPrivateGraphForEquals(option);
return equalsPruned(row.prune(privateGraphForEquals), row2.prune(privateGraphForEquals)).isEqual();
}
private final SQLRowValues getPrivateGraphForEquals(final EqualOption option) {
// don't include joins as we only add those required by "option"
final SQLRowValues res = this.createGraph(option.fields, option.isIgnoreNotDeepCopied() ? PrivateMode.DEEP_COPIED_PRIVATES : PrivateMode.ALL_PRIVATES, false);
for (final SQLRowValues item : new HashSet<SQLRowValues>(res.getGraph().getItems())) {
final SQLElement elem = getElement(item.getTable());
// remove parent
final SQLElementLink parentLink = elem.getParentLink();
setLink(item, parentLink, option.isParentTested());
// remove non shared normal links
// add shared normal links (if join)
for (final SQLElementLink normalLink : elem.getOwnedLinks().getByType(LinkType.ASSOCIATION)) {
setLink(item, normalLink, option.isNonSharedTested() || normalLink.getOwned().isShared());
}
}
return res;
}
private final void setLink(final SQLRowValues item, final SQLElementLink link, final boolean shouldBeTested) {
if (link == null)
return;
if (shouldBeTested) {
if (link.isJoin()) {
assert link.getPath().getStep(0).getDirection() == Direction.REFERENT;
item.assurePath(link.getPath().minusLast()).fillWith(null, false);
}
} else {
if (!link.isJoin()) {
item.removeForeignKey(link.getSingleLink());
}
}
}
static private DiffResult equalsPruned(SQLRowValues row, SQLRowValues row2) {
// neither use order nor PK (don't just remove PK since we need them for
// DiffResult.fillRowMap())
return row.getGraph().getFirstDifference(row, row2, false, false, false);
}
public boolean equalsRecursive(SQLRow row, SQLRow row2) throws SQLException {
return this.equalsRecursive(row, row2, EqualOption.ALL);
}
/**
* Test those rows and all their descendants.
*
* @param row first row.
* @param row2 second row.
* @param option how to compare each descendant, note that #{@link EqualOption#isParentTested()}
* is only meaningful for the passed (root) rows, since descendants are found through
* their parents (i.e. they always have equal parents).
* @return true if both trees are equal according to <code>option</code>.
* @throws SQLException if an error occurs.
*/
public boolean equalsRecursive(SQLRow row, SQLRow row2, EqualOption option) throws SQLException {
// if (!equals(row, row2))
// return false;
return new SQLElementRowR(this, row).equals(new SQLElementRowR(this, row2), option);
}
// no need for equals()/hashCode() since there's only one SQLElement per table and directory
@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.add(id, t);
}
public final void removeComponentFactory(final String id, final ITransformer<Tuple2<SQLElement, String>, SQLComponent> t) {
if (t == null)
throw new NullPointerException();
this.components.removeOne(id, t);
}
private final SQLComponent createComponentFromFactory(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 = 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, never <code>null</code>.
* @throws IllegalStateException if no component is found.
*/
public final SQLComponent createComponent(final String id) throws IllegalStateException {
return this.createComponent(id, true);
}
/**
* 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.
* @param required <code>true</code> if the result cannot be <code>null</code>.
* @return the component or <code>null</code> if all factories return <code>null</code> and
* <code>required</code> is <code>false</code>.
* @throws IllegalStateException if <code>required</code> and no component is found.
*/
public final SQLComponent createComponent(final String id, final boolean required) throws IllegalStateException {
SQLComponent res = this.createComponentFromFactory(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.createComponentFromFactory(id, true);
}
}
if (res != null)
res.setCode(id);
else if (required)
throw new IllegalStateException("No component for " + 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, null);
}
public final boolean putAdditionalTextField(final String field, final Supplier<? extends JTextComponent> comp) {
return this.putAdditionalField(field, comp);
}
public final boolean putAdditionalTextCombo(final String field, final Supplier<? extends SQLTextCombo> comp) {
return this.putAdditionalField(field, comp);
}
public final boolean putAdditionalCombo(final String field, final Supplier<? extends SQLRequestComboBox> comp) {
return this.putAdditionalField(field, comp);
}
// private as only a few JComponent are OK
private final boolean putAdditionalField(final String field, final Supplier<? extends JComponent> comp) {
if (this.additionalFields.containsKey(field)) {
return false;
} else {
this.additionalFields.put(field, comp);
return true;
}
}
public final Map<String, Supplier<? extends 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 Value.hasValue(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>null</code> if there was an error (already presented to the user),
* {@link Value#hasValue() a value} if the user agreed, none if the user refused.
* @deprecated this methods mixes DB and UI access.
*/
public final Value<TreesOfSQLRows> askArchive(final Component comp, final Collection<? extends Number> ids) {
final TreesOfSQLRows trees = TreesOfSQLRows.createFromIDs(this, ids);
try {
trees.fetch(LockStrength.NONE);
final Boolean agreed = this.ask(comp, trees);
if (agreed == null) {
return null;
} else if (agreed) {
this.archive(trees, true);
return Value.getSome(trees);
} else {
return Value.getNone();
}
} catch (SQLException e) {
ExceptionHandler.handle(comp, TM.tr("sqlElement.archiveError", this, ids), e);
return null;
}
}
/**
* Ask the user about rows to archive.
*
* @param comp the parent component.
* @param trees which rows to archive.
* @return <code>null</code> if there was an error (already presented to the user),
* <code>true</code> if the user agreed, <code>false</code> if the user refused.
*/
public Boolean ask(final Component comp, final TreesOfSQLRows trees) {
boolean shouldArchive = false;
if (!trees.isFetched())
throw new IllegalStateException("Trees not yet fetched");
try {
final int rowCount = trees.getTrees().size();
if (rowCount == 0)
return true;
// only check rights if there's actually some rows to delete
if (!UserRightsManager.getCurrentUserRights().canDelete(getTable()))
throw new SQLException("forbidden");
// only display main rows since the user might not be aware of the private ones (the UI
// might hide the fact that one panel is in fact multiple rows)
final Map<SQLTable, List<SQLRowAccessor>> descs = trees.getDescendantsByTable();
final SortedMap<LinkToCut, Integer> externRefs = trees.getExternReferences().countByLink();
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;
}
}
return shouldArchive;
} catch (Exception e) {
ExceptionHandler.handle(comp, TM.tr("sqlElement.rowsToArchiveError", this), e);
return null;
}
}
private final String toString(Map<SQLTable, List<SQLRowAccessor>> descs) {
final List<String> l = new ArrayList<String>(descs.size());
for (final Entry<SQLTable, List<SQLRowAccessor>> e : descs.entrySet()) {
final SQLTable t = e.getKey();
final SQLElement elem = getElement(t);
l.add(elemToString(e.getValue().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<LinkToCut, Integer> externRefs) {
final List<String> l = new ArrayList<String>();
final Map<String, Object> map = new HashMap<String, Object>(4);
for (final Entry<LinkToCut, Integer> entry : externRefs.entrySet()) {
final LinkToCut foreignKey = entry.getKey();
final int count = entry.getValue();
final String label = foreignKey.getLabel();
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);
}
}