OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Compare Revisions

Regard whitespace Rev 81 → Rev 83

/trunk/OpenConcerto/src/org/openconcerto/record/Record.java
New file
0,0 → 1,97
/*
* 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.record;
 
import org.openconcerto.utils.CompareUtils;
 
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
public final class Record {
 
private final String spec;
private RecordKey key;
 
private final Map<String, Object> items;
 
public Record(final String spec, final RecordKey key) {
super();
if (spec == null)
throw new NullPointerException("No spec");
this.spec = spec;
this.key = key;
this.items = new HashMap<String, Object>();
}
 
public final String getSpec() {
return this.spec;
}
 
public final RecordKey getKey() {
return this.key;
}
 
public final void setKey(RecordKey key) {
this.key = key;
}
 
public final Map<String, Object> getItems() {
return this.items;
}
 
public final <T> List<T> getAsList(final String name, Class<T> clazz) {
final Object val = this.getItems().get(name);
if (val == null)
return null;
if (val instanceof List) {
for (final Object o : (List<?>) val) {
clazz.cast(o);
}
@SuppressWarnings("unchecked")
final List<T> res = (List<T>) val;
return res;
} else {
return Collections.singletonList(clazz.cast(val));
}
}
 
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + this.items.hashCode();
result = prime * result + ((this.key == null) ? 0 : this.key.hashCode());
result = prime * result + this.spec.hashCode();
return result;
}
 
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final Record other = (Record) obj;
return this.spec.equals(other.spec) && CompareUtils.equals(this.key, other.key) && this.items.equals(other.items);
}
 
@Override
public String toString() {
return this.getClass().getSimpleName() + ":" + this.spec + (this.getKey() == null ? "" : "[" + this.getKey().getValue() + "]");
}
}
/trunk/OpenConcerto/src/org/openconcerto/record/ConstraintProperties.java
New file
0,0 → 1,23
/*
* 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.record;
 
public final class ConstraintProperties {
public static final String MIN_SIZE = "MIN_SIZE";
public static final String MAX_SIZE = "MAX_SIZE";
public static final String DECIMAL_DIGITS = "DECIMAL_DIGITS";
public static final String MIN_VALUE = "MIN_VALUE";
public static final String MAX_VALUE = "MAX_VALUE";
public static final String REGEXP = "REGEXP";
}
/trunk/OpenConcerto/src/org/openconcerto/record/Constraint.java
New file
0,0 → 1,20
/*
* 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.record;
 
public interface Constraint {
public boolean check(final Object obj);
 
public String getDeclarativeForm();
}
/trunk/OpenConcerto/src/org/openconcerto/record/RecordIO.java
New file
0,0 → 1,34
/*
* 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.record;
 
import java.io.IOException;
import java.util.Set;
 
public interface RecordIO {
 
public Record fetch(final RecordKey key);
 
public Record fetch(final RecordKey key, final Set<String> items);
 
public Record insert(final Record r) throws IOException;
 
public void update(final Record r) throws IOException;
 
public Record copy(final RecordKey key) throws IOException;
public Record copy(final RecordKey key, final boolean forceFull) throws IOException;
 
public void delete(final RecordKey key) throws IOException;
}
/trunk/OpenConcerto/src/org/openconcerto/record/Constraints.java
New file
0,0 → 1,61
/*
* 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.record;
 
import org.openconcerto.utils.checks.EmptyObjFromVO;
 
public final class Constraints {
 
static private final Constraint NONE = new Constraint() {
@Override
public final boolean check(final Object obj) {
return true;
}
 
@Override
public String getDeclarativeForm() {
return "true";
}
 
@Override
public String toString() {
return "no constraint";
}
};
 
public static final Constraint none() {
return NONE;
}
 
static private final Constraint EMPTY = new Constraint() {
@Override
public final boolean check(final Object obj) {
return EmptyObjFromVO.getDefaultPredicate().evaluateChecked(obj);
}
 
@Override
public String getDeclarativeForm() {
return ". = ''";
}
 
@Override
public String toString() {
return "default empty constraint";
}
};
 
public static final Constraint getDefaultEmpty() {
return EMPTY;
}
}
/trunk/OpenConcerto/src/org/openconcerto/record/RecordKey.java
New file
0,0 → 1,58
/*
* 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.record;
 
import net.jcip.annotations.Immutable;
 
@Immutable
public class RecordKey {
 
private final Object val;
 
public RecordKey(Object val) {
super();
if (val == null)
throw new NullPointerException("Null value");
this.val = val;
}
 
public final Object getValue() {
return this.val;
}
 
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + this.val.hashCode();
return result;
}
 
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final RecordKey other = (RecordKey) obj;
return this.val.equals(other.val);
}
 
@Override
public String toString() {
return this.getClass().getSimpleName() + " " + this.getValue();
}
}
/trunk/OpenConcerto/src/org/openconcerto/record/spec/RecordItemSpecBuilder.java
New file
0,0 → 1,129
/*
* 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.record.spec;
 
import org.openconcerto.record.Constraint;
 
import java.util.HashMap;
import java.util.Map;
 
public final class RecordItemSpecBuilder {
 
private final String name;
private final Type type;
private int maxListSize;
private String referencedSpec;
private Object defaultValue;
private boolean required;
private Constraint isValid, isEmpty;
private final Map<String, Object> validProps;
private boolean userMustCheck, userMustModify;
 
public RecordItemSpecBuilder(final String name, final Type type) {
super();
if (name == null)
throw new NullPointerException("Null value");
this.name = name;
this.type = type;
this.maxListSize = 0;
this.validProps = new HashMap<String, Object>(8);
}
 
public String getName() {
return this.name;
}
 
public Type getType() {
return this.type;
}
 
public int getMaxListSize() {
return this.maxListSize;
}
 
public void setMaxListSize(int maxListSize) {
this.maxListSize = maxListSize;
}
 
public String getReferencedSpec() {
return this.referencedSpec;
}
 
public void setReferencedSpec(String referencedSpec) {
this.referencedSpec = referencedSpec;
}
 
public Object getDefaultValue() {
return this.defaultValue;
}
 
public RecordItemSpecBuilder setDefaultValue(Object defaultValue) {
this.defaultValue = defaultValue;
return this;
}
 
public boolean isRequired() {
return this.required;
}
 
public RecordItemSpecBuilder setRequired(boolean required) {
this.required = required;
return this;
}
 
public Constraint getValidConstraint() {
return this.isValid;
}
 
public RecordItemSpecBuilder setValidConstraint(Constraint isValid) {
this.isValid = isValid;
return this;
}
 
public Map<String, Object> getValidProps() {
return this.validProps;
}
 
public Constraint getEmptyConstraint() {
return this.isEmpty;
}
 
public RecordItemSpecBuilder setEmptyConstraint(Constraint isEmpty) {
this.isEmpty = isEmpty;
return this;
}
 
public boolean mustUserCheck() {
return this.userMustCheck;
}
 
public RecordItemSpecBuilder setUserMustCheck(boolean userMustCheck) {
this.userMustCheck = userMustCheck;
return this;
}
 
public boolean mustUserModify() {
return this.userMustModify;
}
 
public RecordItemSpecBuilder setUserMustModify(boolean userMustModify) {
this.userMustModify = userMustModify;
return this;
}
 
public final RecordItemSpec build() {
return new RecordItemSpec(this.name, this.type, this.maxListSize, this.referencedSpec, this.defaultValue, this.required, this.isValid, this.validProps, this.isEmpty, this.userMustCheck,
this.userMustModify);
}
}
/trunk/OpenConcerto/src/org/openconcerto/record/spec/Type.java
New file
0,0 → 1,78
/*
* 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.record.spec;
 
import org.openconcerto.record.Record;
import org.openconcerto.record.RecordRef;
 
import java.math.BigDecimal;
import java.sql.Time;
import java.util.Date;
 
import net.jcip.annotations.Immutable;
 
@Immutable
public enum Type {
STRING(String.class), INTEGER(Number.class), DECIMAL(BigDecimal.class), FLOAT(Number.class), BOOLEAN(Boolean.class), DATETIME(Date.class), TIME(Time.class), RECORD(Record.class), RECORD_REF(
RecordRef.class) {
@Override
public boolean check(Object obj) {
return super.check(obj) || RECORD.check(obj);
}
},
RECORD_LIST(Iterable.class, true) {
@Override
public boolean check(Object obj) {
return checkList(obj, RECORD);
}
},
RECORD_REF_LIST(Iterable.class, true) {
@Override
public boolean check(Object obj) {
return checkList(obj, RECORD_REF);
}
};
 
private final Class<?> primaryClass;
private final boolean containsOrReferToRecord;
 
private Type(final Class<?> primaryClass) {
this(primaryClass, primaryClass == Record.class || primaryClass == RecordRef.class);
}
 
private Type(final Class<?> primaryClass, final boolean containsOrReferToRecord) {
this.primaryClass = primaryClass;
this.containsOrReferToRecord = containsOrReferToRecord;
}
 
public final boolean isRecordType() {
return this.containsOrReferToRecord;
}
 
public boolean check(final Object obj) {
return this.primaryClass.isInstance(obj);
}
 
static private boolean checkList(final Object obj, final Type scalarType) {
if (scalarType.check(obj))
return true;
if (!(obj instanceof Iterable))
return false;
for (final Object item : (Iterable<?>) obj) {
if (!scalarType.check(item))
return false;
}
return true;
}
}
/trunk/OpenConcerto/src/org/openconcerto/record/spec/RecordItemSpec.java
New file
0,0 → 1,163
/*
* 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.record.spec;
 
import org.openconcerto.record.Constraint;
import org.openconcerto.record.Constraints;
import org.openconcerto.record.Record;
import org.openconcerto.record.RecordRef;
 
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
 
import net.jcip.annotations.Immutable;
 
@Immutable
public final class RecordItemSpec {
 
static public enum Problem {
TYPE, VALIDITY, EMPTINESS
}
 
private final String name;
private final Type type;
private final int maxListSize;
private final String referencedSpec;
private final Object defaultValue;
private final boolean required;
private final Constraint isValid, isEmpty;
private final Map<String, Object> validProps;
private final boolean userMustCheck, userMustModify;
 
public RecordItemSpec(final String name, final Type type) {
this(name, type, null);
}
 
public RecordItemSpec(final String name, final Type type, final Constraint isValid) {
this(name, type, 0, null, null, false, null, null, null, false, false);
}
 
RecordItemSpec(String name, Type type, final int maxListSize, final String referencedSpec, Object defaultValue, boolean required, Constraint isValid, Map<String, Object> validProps,
Constraint isEmpty, boolean userMustCheck, boolean userMustModify) {
super();
if (name == null)
throw new NullPointerException("Null value");
this.name = name;
if (type == null)
throw new NullPointerException("Null type");
this.type = type;
if (maxListSize < 0)
throw new IllegalArgumentException("Negative max size : " + maxListSize);
this.maxListSize = maxListSize;
if ((referencedSpec != null) != (this.getType().isRecordType()))
throw new IllegalArgumentException("Invalid referencedSpec for " + this.getType() + " : " + referencedSpec);
this.referencedSpec = referencedSpec;
this.defaultValue = defaultValue;
this.required = required;
this.isValid = isValid == null ? Constraints.none() : isValid;
this.validProps = validProps == null ? Collections.<String, Object> emptyMap() : Collections.unmodifiableMap(new HashMap<String, Object>(validProps));
this.isEmpty = isEmpty == null ? Constraints.getDefaultEmpty() : isEmpty;
this.userMustCheck = userMustCheck;
this.userMustModify = userMustModify;
}
 
public final String getName() {
return this.name;
}
 
public final Type getType() {
return this.type;
}
 
/**
* The maximum count of elements.
*
* @return the maximum, never negative, <code>0</code> meaning not a list.
*/
public final int getMaxListSize() {
return this.maxListSize;
}
 
public final String getReferencedSpec() {
return this.referencedSpec;
}
 
public final Object getDefaultValue() {
return this.defaultValue;
}
 
public final Constraint getValidConstraint() {
return this.isValid;
}
 
public final Map<String, Object> getValidProperties() {
return this.validProps;
}
 
public final Constraint getEmptyConstraint() {
return this.isEmpty;
}
 
public final boolean isRequired() {
return this.required;
}
 
public final Set<Problem> check(final Object obj) {
if (obj != null) {
if (!this.getType().check(obj))
return EnumSet.of(Problem.TYPE);
if (this.getReferencedSpec() != null) {
final Collection<?> c = obj instanceof Collection ? (Collection<?>) obj : Collections.singleton(obj);
for (final Object o : c) {
final String foreignSpec;
if (o instanceof RecordRef)
foreignSpec = ((RecordRef) o).getSpec().getName();
else
foreignSpec = ((Record) o).getSpec();
if (!foreignSpec.equals(this.getReferencedSpec()))
return EnumSet.of(Problem.TYPE);
}
}
if (this.getMaxListSize() > 0) {
if (!(obj instanceof Collection)) {
return EnumSet.of(Problem.VALIDITY);
} else if (((Collection<?>) obj).size() > this.getMaxListSize()) {
return EnumSet.of(Problem.VALIDITY);
}
}
}
if (!this.getValidConstraint().check(obj))
return EnumSet.of(Problem.VALIDITY);
if (this.isRequired() && this.getEmptyConstraint().check(obj))
return EnumSet.of(Problem.EMPTINESS);
return EnumSet.noneOf(Problem.class);
}
 
public final boolean isUserMustCheck() {
return this.userMustCheck;
}
 
public final boolean isUserMustModify() {
return this.userMustModify;
}
 
@Override
public String toString() {
return this.getClass().getSimpleName() + " '" + this.getName() + "' ; type : " + this.getType() + " ; constraint : " + this.getValidConstraint();
}
}
/trunk/OpenConcerto/src/org/openconcerto/record/spec/RecordSpec.java
New file
0,0 → 1,91
/*
* 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.record.spec;
 
import org.openconcerto.record.Constraint;
import org.openconcerto.record.Constraints;
import org.openconcerto.record.Record;
import org.openconcerto.record.spec.RecordItemSpec.Problem;
 
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
 
import net.jcip.annotations.Immutable;
 
@Immutable
public final class RecordSpec {
 
private final String name;
private final Map<String, RecordItemSpec> items;
private final Constraint constraint;
 
public RecordSpec(final String name, final Collection<RecordItemSpec> items, final Constraint c) {
super();
if (name == null)
throw new NullPointerException("Null value");
this.name = name;
final Map<String, RecordItemSpec> map = new LinkedHashMap<String, RecordItemSpec>(items.size());
for (final RecordItemSpec itemSpec : items) {
if (map.put(itemSpec.getName(), itemSpec) != null)
throw new IllegalArgumentException("Duplicate name : " + itemSpec.getName());
}
this.items = Collections.unmodifiableMap(map);
this.constraint = c == null ? Constraints.none() : c;
}
 
public final String getName() {
return this.name;
}
 
public final Map<String, RecordItemSpec> getItems() {
return this.items;
}
 
public final Constraint getValidConstraint() {
return this.constraint;
}
 
public final Map<String, Set<Problem>> check(final Record record) {
return this.check(record, false);
}
 
public final Map<String, Set<Problem>> check(final Record record, final boolean allowPartial) {
if (!record.getSpec().equals(this.getName()))
throw new IllegalArgumentException("Name mismatch '" + record.getSpec() + "' != '" + this.getName() + "'");
final Map<String, Set<Problem>> res = new HashMap<String, Set<Problem>>();
for (final Entry<String, RecordItemSpec> e : this.getItems().entrySet()) {
final String itemName = e.getKey();
final RecordItemSpec itemSpec = e.getValue();
if (!allowPartial || record.getItems().containsKey(itemName)) {
final Set<Problem> pbs = itemSpec.check(record.getItems().get(itemName));
if (!pbs.isEmpty())
res.put(itemName, pbs);
}
}
if (!(allowPartial || this.getValidConstraint().check(record)) || !this.getItems().keySet().containsAll(record.getItems().keySet()))
res.put(null, EnumSet.of(Problem.VALIDITY));
return res;
}
 
@Override
public String toString() {
return this.getClass().getSimpleName() + " '" + this.getName() + "' ; constraint : " + this.getValidConstraint() + " ; items : \n" + this.getItems().values();
}
}
/trunk/OpenConcerto/src/org/openconcerto/record/RecordRef.java
New file
0,0 → 1,63
/*
* 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.record;
 
import org.openconcerto.record.spec.RecordSpec;
import net.jcip.annotations.Immutable;
 
@Immutable
public final class RecordRef {
 
private final RecordSpec spec;
private final RecordKey key;
 
public RecordRef(RecordSpec spec, RecordKey key) {
super();
if (spec == null)
throw new NullPointerException("Null spec");
this.spec = spec;
if (key == null)
throw new NullPointerException("Null key");
this.key = key;
}
 
public final RecordSpec getSpec() {
return this.spec;
}
 
public final RecordKey getKey() {
return this.key;
}
 
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + this.key.hashCode();
result = prime * result + this.spec.hashCode();
return result;
}
 
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
RecordRef other = (RecordRef) obj;
return this.key.equals(other.key) && this.spec.equals(other.spec);
}
}
/trunk/OpenConcerto/src/org/openconcerto/sql/ShowAs.java
29,6 → 29,9
import java.util.List;
import java.util.Map;
 
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;
 
/**
* Gère la représentation des clefs externes. TODO Pour ne pas avoir la désignation du site :
* pouvoir spécifier ELEMENT_TABLEAU.ID_TABLEAU_ELECTRIQUE => DESIGNATION,
36,12 → 39,16
*
* @author Sylvain CUAZ
*/
@ThreadSafe
public class ShowAs extends FieldExpander {
 
@GuardedBy("this")
private DBRoot root;
// eg /OBSERVATION/ -> [ID_ARTICLE_1, DESIGNATION]
@GuardedBy("this")
private final Map<SQLTable, List<SQLField>> byTables;
// eg |TABLEAU.ID_OBSERVATION| -> [DESIGNATION]
@GuardedBy("this")
private final Map<SQLField, List<SQLField>> byFields;
 
public ShowAs(DBRoot root) {
52,7 → 59,7
this.setRoot(root);
}
 
public final void putAll(ShowAs s) {
public synchronized final void putAll(ShowAs s) {
CollectionUtils.addIfNotPresent(this.byFields, s.byFields);
CollectionUtils.addIfNotPresent(this.byTables, s.byTables);
// s might have replaced some of our entries
59,7 → 66,7
this.clearCache();
}
 
public List<SQLField> getFieldExpand(SQLTable table) {
public synchronized List<SQLField> getFieldExpand(SQLTable table) {
return this.byTables.get(table);
}
 
68,15 → 75,15
*
* @param root the base to use.
*/
public final void setRoot(DBRoot root) {
public synchronized final void setRoot(DBRoot root) {
this.root = root;
}
 
private SQLField getField(String fieldName) {
private synchronized SQLField getField(String fieldName) {
return this.root.getDesc(SQLName.parse(fieldName), SQLField.class);
}
 
private SQLTable getTable(String tableName) {
private synchronized SQLTable getTable(String tableName) {
try {
return this.root.getDesc(SQLName.parse(tableName), SQLTable.class);
} catch (DBStructureItemNotFound e) {
93,7 → 100,7
}
 
// TODO a listener to remove tables and fields as they are dropped
public final void removeTable(SQLTable t) {
public synchronized final void removeTable(SQLTable t) {
this.byTables.remove(t);
for (final SQLField f : t.getFields())
this.byFields.remove(f);
100,7 → 107,7
this.clearCache();
}
 
public final void clear() {
public synchronized final void clear() {
this.setRoot(null);
this.byTables.clear();
this.byFields.clear();
120,7 → 127,7
* @param tableName le nom de la table, eg "ETABLISSEMENT".
* @param fields les noms des champs, eg ["DESCRIPTION", "NUMERO"].
*/
public void show(String tableName, List<String> fields) {
public synchronized void show(String tableName, List<String> fields) {
final SQLTable table = this.getTable(tableName);
if (table != null) {
this.show(table, fields);
133,7 → 140,7
this.show(table, Arrays.asList(fields));
}
 
public void show(SQLTable table, List<String> fields) {
public synchronized void show(SQLTable table, List<String> fields) {
this.byTables.put(table, namesToFields(fields, table));
this.clearCache();
}
151,11 → 158,11
* @param fieldName le nom du champ, eg "CONTACT.ID_ETABLISSEMENT".
* @param fields les noms des champs, eg ["DESCRIPTION"].
*/
public void showField(String fieldName, List<String> fields) {
public synchronized void showField(String fieldName, List<String> fields) {
this.show(getField(fieldName), fields);
}
 
public final void show(SQLField field, List<String> fields) {
public synchronized final void show(SQLField field, List<String> fields) {
this.byFields.put(field, namesToFields(fields, field.getTable().getBase().getGraph().getForeignTable(field)));
this.clearCache();
}
163,7 → 170,7
// *** expand
 
@Override
protected List<SQLField> expandOnce(SQLField field) {
protected synchronized List<SQLField> expandOnce(SQLField field) {
// c'est une clef externe, donc elle pointe sur une table
final SQLTable foreignTable = field.getTable().getBase().getGraph().getForeignTable(field);
final List<SQLField> res;
184,12 → 191,12
* @param fieldName le nom du champ à expandre, eg "SITE.ID_ETABLISSEMENT".
* @return la liste des champs, eg [|ETABLISSEMENT.DESCRIPTION|, |ETABLISSEMENT.NUMERO|].
*/
public List<SQLField> simpleExpand(String fieldName) {
public synchronized List<SQLField> simpleExpand(String fieldName) {
return this.simpleExpand(getField(fieldName));
}
 
@Override
public String toString() {
public synchronized String toString() {
return super.toString() + " byTables: " + this.byTables + " byFields: " + this.byFields;
}
 
/trunk/OpenConcerto/src/org/openconcerto/sql/sqlobject/ISQLElementWithCodeSelector.java
File deleted
/trunk/OpenConcerto/src/org/openconcerto/sql/sqlobject/SQLRequestComboBox.java
100,6 → 100,7
// le mode actuel
private ComboMode mode;
// le mode à sélectionner à la fin du updateAll
// null when not used
private ComboMode modeToSelect;
 
// to speed up the combo
116,9 → 117,7
public SQLRequestComboBox(boolean addUndefined, int preferredWidthInChar) {
this.setOpaque(false);
this.mode = ComboMode.EDITABLE;
// necessary when uiInit() is called with a model already updating
// (otherwise when it finishes modeToSelect will still be null)
this.modeToSelect = this.mode;
this.modeToSelect = null;
if (preferredWidthInChar > 0) {
final char[] a = new char[preferredWidthInChar];
Arrays.fill(a, ' ');
175,7 → 174,7
this.uiInit(new IComboModel(req));
}
 
private boolean hasModel() {
protected final boolean hasModel() {
return this.req != null;
}
 
192,7 → 191,7
this.req.addListener("updating", new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
updatingChanged((Boolean) evt.getNewValue());
updateEnabled();
}
});
// since modelValueChanged() updates the UI use selectedValue (i.e. IComboSelectionItem
280,8 → 279,21
}
});
 
this.combo.addPropertyChangeListener("textCompFocused", new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
updateEnabled();
}
});
 
this.uiLayout();
 
// Initialise UI : mode was set in the constructor, but the UI wasn't updated (since it is
// either not created or depending on the request). Do it before setRunning() since it might
// trigger setEnabled() and the one below would be unnecessary.
if (!updateEnabled())
this.setEnabled(this.getEnabled());
 
// *without* : resetValue() => doUpdateAll() since it was never filled
// then this is made displayable => setRunning(true) => dirty = true since not on screen
// finally made visible => setOnScreen(true) => second doUpdateAll()
292,16 → 304,37
this.req.setRunning(true);
}
 
private final void updatingChanged(final Boolean newValue) {
if (Boolean.TRUE.equals(newValue)) {
private final boolean updateEnabled() {
boolean res = false;
if (isDisabledState()) {
// don't overwrite, happens if updateEnabled() is called twice and isDisabledState() is
// false both times. In that case getEnabled() would return DISABLED
if (this.modeToSelect == null) {
this.modeToSelect = this.getEnabled();
// ne pas interagir pendant le chargement
this.setEnabled(ComboMode.DISABLED, true);
res = true;
}
} else {
this.setEnabled(this.modeToSelect);
// only use modeToSelect once. If updateEnabled() is called twice and isDisabledState()
// is true both times and modeToSelect wasn't cleared it could be obsolete (another
// setEnabled() could have been called)
if (this.modeToSelect != null) {
final ComboMode m = this.modeToSelect;
this.modeToSelect = null;
this.setEnabled(m, true);
res = true;
}
}
return res;
}
 
// disable combo when updating, except if the user is currently using it (e.g. when using a
// request that search the DB interactively)
private final boolean isDisabledState() {
return this.isUpdating() && !this.combo.getTextComp().isFocusOwner();
}
 
public final List<Action> getActions() {
return this.combo.getActions();
}
479,6 → 512,7
this.setEnabled(b ? ComboMode.EDITABLE : ComboMode.ENABLED);
}
 
@Override
public final void setEnabled(boolean b) {
// FIXME add mode to RIV
this.setEnabled(b ? ComboMode.EDITABLE : ComboMode.ENABLED);
490,7 → 524,7
 
private final void setEnabled(ComboMode mode, boolean priv) {
assert SwingUtilities.isEventDispatchThread();
if (!priv && this.isUpdating()) {
if (!priv && this.isDisabledState()) {
this.modeToSelect = mode;
} else {
this.mode = mode;
/trunk/OpenConcerto/src/org/openconcerto/sql/sqlobject/IComboSelectionItemListener.java
New file
0,0 → 1,18
/*
* 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.sqlobject;
 
public interface IComboSelectionItemListener {
public void itemSelected(IComboSelectionItem selectedItem);
}
/trunk/OpenConcerto/src/org/openconcerto/sql/sqlobject/ITextWithCompletionPopUp.java
41,9 → 41,9
 
private int minWitdh = 150;
 
private ITextWithCompletion text;
private IComboSelectionItemListener text;
 
ITextWithCompletionPopUp(final ListModel listModel, final ITextWithCompletion text) {
ITextWithCompletionPopUp(final ListModel listModel, final IComboSelectionItemListener text) {
this.text = text;
this.list = new JList(listModel);
this.listModel = listModel;
181,7 → 181,7
if (sIndex >= 0) {
IComboSelectionItem item = (IComboSelectionItem) listModel.getElementAt(sIndex);
 
text.selectId(item.getId());
text.itemSelected(item);
}
}
}
/trunk/OpenConcerto/src/org/openconcerto/sql/sqlobject/SelectionRowListener.java
New file
0,0 → 1,20
/*
* 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.sqlobject;
 
import org.openconcerto.sql.model.SQLRowAccessor;
 
public interface SelectionRowListener {
public void rowSelected(SQLRowAccessor row, Object source);
}
/trunk/OpenConcerto/src/org/openconcerto/sql/sqlobject/ITextWithCompletion.java
57,7 → 57,7
import javax.swing.text.DocumentFilter;
import javax.swing.text.JTextComponent;
 
public class ITextWithCompletion extends JPanel implements DocumentListener, TextComponent, MutableValueObject<String> {
public class ITextWithCompletion extends JPanel implements DocumentListener, TextComponent, MutableValueObject<String>, IComboSelectionItemListener {
 
// FIXME asynchronous completion
 
452,6 → 452,11
 
}
 
@Override
public void itemSelected(IComboSelectionItem item) {
selectId(item.getId());
}
 
public synchronized void selectId(int id) {
 
if (this.isLoading) {
/trunk/OpenConcerto/src/org/openconcerto/sql/sqlobject/SQLTextCombo.java
55,7 → 55,7
return "LABEL";
}
 
static public final SQLCreateMoveableTable getCreateTable(SQLSyntax syntax) {
static public final SQLCreateMoveableTable getCreateTable(final SQLSyntax syntax) {
final SQLCreateMoveableTable createTable = new SQLCreateMoveableTable(syntax, getTableName());
createTable.addVarCharColumn(getRefFieldName(), 100);
createTable.addVarCharColumn(getValueFieldName(), 200);
67,16 → 67,16
super();
}
 
public SQLTextCombo(boolean locked) {
public SQLTextCombo(final boolean locked) {
super(locked);
}
 
public SQLTextCombo(ComboLockedMode mode) {
public SQLTextCombo(final ComboLockedMode mode) {
super(mode);
}
 
@Override
public void init(SQLRowItemView v) {
public void init(final SQLRowItemView v) {
if (!this.hasCache()) {
final ITextComboCacheSQL cache = new ITextComboCacheSQL(v.getField());
if (cache.isValid())
84,12 → 84,9
}
}
 
static public class ITextComboCacheSQL implements ITextComboCache {
static public class ITextComboCacheSQL extends AbstractComboCacheSQL {
 
private final String field;
private final SQLTable t;
private final List<String> cache;
private boolean loadedOnce;
private final String id;
 
public ITextComboCacheSQL(final SQLField f) {
this(f.getDBRoot(), f.getFullName());
96,28 → 93,109
}
 
public ITextComboCacheSQL(final DBRoot r, final String id) {
this.field = id;
this.t = r.findTable(getTableName());
if (!this.isValid())
Log.get().warning("no completion found for " + this.field);
this.cache = new ArrayList<String>();
this.loadedOnce = false;
super(r.findTable(getTableName()), getValueFieldName());
this.id = id;
}
 
@Override
public final boolean isValid() {
return this.t != null;
return this.getTable() != null;
}
 
private final SQLDataSource getDS() {
return this.t.getDBSystemRoot().getDataSource();
@Override
protected Where createWhere() {
return new Where(this.getTable().getField(getRefFieldName()), "=", this.id);
}
 
@Override
public void addToCache(final String string) {
if (!this.cache.contains(string)) {
final Map<String, Object> m = new HashMap<String, Object>();
m.put(getRefFieldName(), this.id);
m.put(getValueFieldName(), string);
try {
// the primary key is not generated so don't let SQLRowValues remove it.
new SQLRowValues(this.getTable(), m).insert(true, false);
} catch (final SQLException e) {
// e.g. some other VM hasn't already added it
e.printStackTrace();
}
// add anyway since we didn't contain it
this.cache.add(string);
}
}
 
@Override
public void deleteFromCache(final String string) {
final Where w = new Where(this.getTable().getField(getRefFieldName()), "=", this.id).and(new Where(this.getField(), "=", string));
this.getDS().executeScalar("DELETE FROM " + this.getTable().getSQLName().quote() + " WHERE " + w.getClause());
this.cache.removeAll(Collections.singleton(string));
}
 
@Override
public String toString() {
return super.toString() + "/" + this.id;
}
}
 
static public class ITextComboCacheExistingValues extends AbstractComboCacheSQL {
 
public ITextComboCacheExistingValues(final SQLField f) {
super(f);
}
 
@Override
public boolean isValid() {
return String.class.isAssignableFrom(this.getField().getType().getJavaType());
}
 
@Override
protected Where createWhere() {
return null;
}
 
@Override
public void addToCache(final String string) {
throw new UnsupportedOperationException();
}
 
@Override
public void deleteFromCache(final String string) {
throw new UnsupportedOperationException();
}
}
 
// a cache that takes values from an SQL field
static public abstract class AbstractComboCacheSQL implements ITextComboCache {
 
// values are in this field (e.g. COMPLETION.LABEL)
private final SQLField f;
protected final List<String> cache;
private boolean loadedOnce;
 
protected AbstractComboCacheSQL(final SQLTable t, final String fieldName) {
this(t == null ? null : t.getField(fieldName));
}
 
protected AbstractComboCacheSQL(final SQLField f) {
this.f = f;
this.cache = new ArrayList<String>();
this.loadedOnce = false;
if (!this.isValid())
Log.get().warning("no completion found for " + this);
}
 
protected final SQLDataSource getDS() {
return this.f.getDBSystemRoot().getDataSource();
}
 
@Override
public List<String> loadCache(final boolean dsCache) {
final SQLSelect sel = new SQLSelect();
sel.addSelect(this.t.getField(getValueFieldName()));
sel.setWhere(new Where(this.t.getField(getRefFieldName()), "=", this.field));
sel.addSelect(getField());
sel.setWhere(createWhere());
// predictable order
sel.addFieldOrder(this.t.getField(getValueFieldName()));
sel.addFieldOrder(getField());
// ignore DS cache to allow the fetching of rows modified by another VM
@SuppressWarnings("unchecked")
final List<String> items = (List<String>) this.getDS().execute(sel.asString(), new IResultSetHandler(SQLDataSource.COLUMN_LIST_HANDLER) {
137,6 → 215,17
return this.cache;
}
 
protected final SQLTable getTable() {
return this.getField().getTable();
}
 
protected final SQLField getField() {
return this.f;
}
 
protected abstract Where createWhere();
 
@Override
public List<String> getCache() {
if (!this.loadedOnce) {
this.loadCache(true);
145,32 → 234,9
return this.cache;
}
 
public void addToCache(String string) {
if (!this.cache.contains(string)) {
final Map<String, Object> m = new HashMap<String, Object>();
m.put(getRefFieldName(), this.field);
m.put(getValueFieldName(), string);
try {
// the primary key is not generated so don't let SQLRowValues remove it.
new SQLRowValues(this.t, m).insert(true, false);
} catch (SQLException e) {
// e.g. some other VM hasn't already added it
e.printStackTrace();
}
// add anyway since we didn't contain it
this.cache.add(string);
}
}
 
public void deleteFromCache(String string) {
final Where w = new Where(this.t.getField(getRefFieldName()), "=", this.field).and(new Where(this.t.getField(getValueFieldName()), "=", string));
this.getDS().executeScalar("DELETE FROM " + this.t.getSQLName().quote() + " WHERE " + w.getClause());
this.cache.removeAll(Collections.singleton(string));
}
 
@Override
public String toString() {
return this.getClass().getName() + " on " + this.field;
return this.getClass().getName() + " on " + this.getField();
}
 
}
/trunk/OpenConcerto/src/org/openconcerto/sql/sqlobject/ITextArticleWithCompletion.java
New file
0,0 → 1,633
/*
* 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.sqlobject;
 
import org.openconcerto.sql.model.SQLField;
import org.openconcerto.sql.model.SQLRow;
import org.openconcerto.sql.model.SQLRowAccessor;
import org.openconcerto.sql.model.SQLSelect;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.Where;
import org.openconcerto.sql.request.MultipleSQLSelectExecutor;
import org.openconcerto.ui.component.text.TextComponent;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.OrderedSet;
import org.openconcerto.utils.SwingWorker2;
import org.openconcerto.utils.checks.MutableValueObject;
import org.openconcerto.utils.model.DefaultIMutableListModel;
import org.openconcerto.utils.text.DocumentFilterList;
import org.openconcerto.utils.text.DocumentFilterList.FilterType;
import org.openconcerto.utils.text.LimitedSizeDocumentFilter;
 
import java.awt.Component;
import java.awt.GridLayout;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import java.util.Vector;
 
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AbstractDocument;
import javax.swing.text.DocumentFilter;
import javax.swing.text.JTextComponent;
 
public class ITextArticleWithCompletion extends JPanel implements DocumentListener, TextComponent, MutableValueObject<String>, IComboSelectionItemListener {
 
public static int SQL_RESULT_LIMIT = 50;
 
public static final int MODE_STARTWITH = 1;
public static final int MODE_CONTAINS = 2;
 
private JTextComponent text;
 
private DefaultIMutableListModel<IComboSelectionItem> model = new DefaultIMutableListModel<IComboSelectionItem>();
 
private boolean completionEnabled = true;
 
private SQLRowAccessor selectedRow = null;
 
private boolean selectAuto = true;
 
protected ITextWithCompletionPopUp popup;
 
OrderedSet<SelectionRowListener> listeners = new OrderedSet<SelectionRowListener>();
Component popupInvoker;
 
private boolean isLoading = false;
private SQLRowAccessor rowToSelect = null;
 
private String fillWith = "CODE";
private final PropertyChangeSupport supp;
 
private final SQLTable tableArticle, tableArticleFournisseur;
 
// Asynchronous filling
private Thread searchThread;
private int autoCheckDelay = 1000;
private boolean disposed = false;
private Stack<String> searchStack = new Stack<String>();
private boolean autoselectIfMatch;
private static final int PAUSE_MS = 150;
 
public ITextArticleWithCompletion(SQLTable tableArticle, SQLTable tableARticleFournisseur) {
this.tableArticle = tableArticle;
this.tableArticleFournisseur = tableARticleFournisseur;
this.supp = new PropertyChangeSupport(this);
this.popup = new ITextWithCompletionPopUp(this.model, this);
this.text = new JTextField();
this.setLayout(new GridLayout(1, 1));
this.add(this.text);
setTextEditor(this.text);
setPopupInvoker(this);
 
//
disposed = false;
searchThread = new Thread() {
public void run() {
while (!disposed) {
if (autoCheckDelay == 0) {
autoCheckDelay = -1;
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
loadAutoCompletion();
}
});
 
} else if (autoCheckDelay > 0) {
autoCheckDelay -= PAUSE_MS;
}
try {
Thread.sleep(PAUSE_MS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
 
};
};
searchThread.setName("ITextArticleWithCompletion thread");
searchThread.setPriority(Thread.MIN_PRIORITY);
searchThread.setDaemon(true);
searchThread.start();
 
}
 
public void setPopupListEnabled(boolean b) {
this.popup.setListEnabled(b);
}
 
public void setTextEditor(final JTextComponent atext) {
if (atext == null) {
throw new IllegalArgumentException("null textEditor");
}
this.text = atext;
atext.getDocument().addDocumentListener(this);
atext.addKeyListener(new KeyListener() {
 
private boolean consume;
 
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_TAB) {
// Complete si exactement la valeur souhaitée
updateAutoCompletion(true);
e.consume();
} else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
if (ITextArticleWithCompletion.this.popup.isShowing()) {
ITextArticleWithCompletion.this.popup.selectNext();
e.consume();
} else {
if (getSelectedRow() == null) {
// updateAutoCompletion();
showPopup();
}
}
 
} else if (e.getKeyCode() == KeyEvent.VK_UP) {
if (ITextArticleWithCompletion.this.popup.isShowing()) {
ITextArticleWithCompletion.this.popup.selectPrevious();
e.consume();
}
 
} else if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_TAB) {
if (ITextArticleWithCompletion.this.popup.isShowing()) {
ITextArticleWithCompletion.this.popup.validateSelection();
e.consume();
} else {
autoselectIfMatch = true;
e.consume();
}
} else if (e.getKeyCode() == KeyEvent.VK_PAGE_DOWN) {
if (ITextArticleWithCompletion.this.popup.isShowing()) {
ITextArticleWithCompletion.this.popup.selectNextPage();
e.consume();
}
} else if (e.getKeyCode() == KeyEvent.VK_PAGE_UP) {
if (ITextArticleWithCompletion.this.popup.isShowing()) {
ITextArticleWithCompletion.this.popup.selectPreviousPage();
e.consume();
}
} else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
if (ITextArticleWithCompletion.this.popup.isShowing()) {
hidePopup();
}
 
}
 
// else {
// if (e.getKeyCode() != KeyEvent.VK_RIGHT && e.getKeyCode() !=
// KeyEvent.VK_LEFT) {
// fireSelectionId(-1);
// }
// }
// Evite les bips
if (ITextArticleWithCompletion.this.text.getDocument().getLength() == 0 && (e.getKeyCode() == KeyEvent.VK_DELETE || e.getKeyCode() == KeyEvent.VK_BACK_SPACE)) {
System.err.println("consume");
this.consume = true;
e.consume();
}
 
}
 
public void keyReleased(KeyEvent e) {
}
 
public void keyTyped(KeyEvent e) {
// Evite les bips
if (this.consume) {
e.consume();
this.consume = false;
}
}
});
this.addComponentListener(new ComponentListener() {
public void componentHidden(ComponentEvent e) {
}
 
public void componentMoved(ComponentEvent e) {
}
 
public void componentResized(ComponentEvent e) {
// ajuste la taille min de la popup
ITextArticleWithCompletion.this.popup.setMinWith(atext.getBounds().width);
}
 
public void componentShown(ComponentEvent e) {
}
});
atext.addFocusListener(new FocusListener() {
@Override
public void focusGained(FocusEvent e) {
 
}
 
@Override
public void focusLost(FocusEvent e) {
hidePopup();
}
});
}
 
/**
* Retourne une liste de IComboSelectionItem, qui sont les selections possibles pour le text
* passé
*
* @throws SQLException
*/
List<IComboSelectionItem> getPossibleValues(String aText) throws SQLException {
List<IComboSelectionItem> result = new Vector<IComboSelectionItem>();
if (aText.isEmpty()) {
return result;
}
aText = aText.trim();
 
if (aText.length() > 0) {
 
List<SQLSelect> listSel = new ArrayList<SQLSelect>();
// CODE ARTICLE = aText
SQLSelect selMatchingCode = new SQLSelect();
// selMatchingCode.addSelectStar(this.tableArticle);
selMatchingCode.addSelect(this.tableArticle.getKey());
selMatchingCode.addSelect(this.tableArticle.getField("CODE"));
selMatchingCode.addSelect(this.tableArticle.getField("NOM"));
selMatchingCode.addSelect(this.tableArticle.getField("CODE_BARRE"));
Where wMatchingCode = new Where(this.tableArticle.getField("CODE"), "=", aText);
wMatchingCode = wMatchingCode.or(new Where(this.tableArticle.getField("NOM"), "=", aText));
wMatchingCode = wMatchingCode.or(new Where(this.tableArticle.getField("CODE_BARRE"), "=", aText));
selMatchingCode.setWhere(wMatchingCode);
listSel.add(selMatchingCode);
 
// CODE ARTICLE LIKE %aText% with limit
SQLSelect selContains = new SQLSelect();
// selContains.addSelectStar(this.tableArticle);
selContains.addSelect(this.tableArticle.getKey());
selContains.addSelect(this.tableArticle.getField("CODE"));
selContains.addSelect(this.tableArticle.getField("NOM"));
selContains.addSelect(this.tableArticle.getField("CODE_BARRE"));
Where wContains = new Where(this.tableArticle.getField("CODE"), "LIKE", "%" + aText + "%");
wContains = wContains.or(new Where(this.tableArticle.getField("NOM"), "LIKE", "%" + aText + "%"));
wContains = wContains.or(new Where(this.tableArticle.getField("CODE_BARRE"), "LIKE", "%" + aText + "%"));
selContains.setWhere(wContains.and(wMatchingCode.not()));
selContains.setLimit(SQL_RESULT_LIMIT);
listSel.add(selContains);
 
// CODE ARTICLE = aText
final Where wNotSync = new Where(this.tableArticleFournisseur.getField("ID_ARTICLE"), "IS", (Object) null).or(new Where(this.tableArticleFournisseur.getField("ID_ARTICLE"), "=",
this.tableArticleFournisseur.getUndefinedID()));
 
SQLSelect selMatchingCodeF = new SQLSelect();
// selMatchingCodeF.addSelectStar(this.tableArticleFournisseur);
selMatchingCodeF.addSelect(this.tableArticleFournisseur.getKey());
selMatchingCodeF.addSelect(this.tableArticleFournisseur.getField("CODE"));
selMatchingCodeF.addSelect(this.tableArticleFournisseur.getField("NOM"));
selMatchingCodeF.addSelect(this.tableArticleFournisseur.getField("CODE_BARRE"));
Where wMatchingCodeF = new Where(this.tableArticleFournisseur.getField("CODE"), "=", aText);
wMatchingCodeF = wMatchingCodeF.or(new Where(this.tableArticleFournisseur.getField("CODE_BARRE"), "=", aText));
wMatchingCodeF = wMatchingCodeF.or(new Where(this.tableArticleFournisseur.getField("NOM"), "=", aText));
selMatchingCodeF.setWhere(wMatchingCodeF.and(wNotSync));
listSel.add(selMatchingCodeF);
 
// CODE ARTICLE_FOURNISSEUR LIKE %aText% with limit
SQLSelect selContainsCodeF = new SQLSelect();
// selContainsCodeF.addSelectStar(this.tableArticleFournisseur);
selContainsCodeF.addSelect(this.tableArticleFournisseur.getKey());
selContainsCodeF.addSelect(this.tableArticleFournisseur.getField("CODE"));
selContainsCodeF.addSelect(this.tableArticleFournisseur.getField("NOM"));
selContainsCodeF.addSelect(this.tableArticleFournisseur.getField("CODE_BARRE"));
Where wContainsCodeF = new Where(this.tableArticleFournisseur.getField("CODE"), "LIKE", "%" + aText + "%");
wContainsCodeF = wContainsCodeF.or(new Where(this.tableArticleFournisseur.getField("CODE_BARRE"), "LIKE", "%" + aText + "%"));
wContainsCodeF = wContainsCodeF.or(new Where(this.tableArticleFournisseur.getField("NOM"), "LIKE", "%" + aText + "%"));
selContainsCodeF.setWhere(wContainsCodeF.and(wMatchingCodeF.not()).and(wNotSync));
selContainsCodeF.setLimit(SQL_RESULT_LIMIT);
 
listSel.add(selContainsCodeF);
 
MultipleSQLSelectExecutor mult = new MultipleSQLSelectExecutor(this.tableArticle.getDBSystemRoot(), listSel);
 
List<List<SQLRow>> resultList = mult.execute();
 
for (List<SQLRow> list : resultList) {
 
for (SQLRow sqlRow : list) {
 
StringBuffer buf = new StringBuffer();
if (sqlRow.getString("CODE_BARRE") != null && sqlRow.getString("CODE_BARRE").trim().length() > 0) {
buf.append(sqlRow.getString("CODE_BARRE") + " -- ");
}
buf.append(sqlRow.getString("CODE") + " -- ");
buf.append(sqlRow.getString("NOM"));
result.add(new IComboSelectionItem(sqlRow, buf.toString()));
}
}
 
}
 
return result;
}
 
private void updateAutoCompletion(boolean autoselectIfMatch) {
this.autoselectIfMatch = autoselectIfMatch;
this.autoCheckDelay = PAUSE_MS * 2;
synchronized (searchStack) {
this.searchStack.push(this.text.getText().trim());
}
}
 
private void loadAutoCompletion() {
if (!this.isCompletionEnabled() || this.isLoading) {
return;
}
final String t;
synchronized (searchStack) {
if (this.searchStack.isEmpty()) {
return;
}
t = this.searchStack.pop();
this.searchStack.clear();
}
 
final SwingWorker2<List<IComboSelectionItem>, Object> worker = new SwingWorker2<List<IComboSelectionItem>, Object>() {
 
@Override
protected List<IComboSelectionItem> doInBackground() throws Exception {
List<IComboSelectionItem> l = getPossibleValues(t); // Liste de IComboSelection
return l;
}
 
@Override
protected void done() {
List<IComboSelectionItem> l;
try {
l = get();
} catch (Exception e) {
l = new ArrayList<IComboSelectionItem>(0);
e.printStackTrace();
}
// On cache la popup si le nombre de ligne change afin que sa taille soit correcte
if (l.size() != model.getSize() && l.size() <= ITextWithCompletionPopUp.MAXROW) {
hidePopup();
}
// on vide le model
model.removeAllElements();
model.addAll(l);
 
if (l.size() > 0) {
showPopup();
} else {
hidePopup();
}
SQLRowAccessor newRow = selectedRow;
boolean found = false;
for (Iterator<IComboSelectionItem> iter = l.iterator(); iter.hasNext();) {
IComboSelectionItem element = iter.next();
if (element.getLabel().toLowerCase().contains(t.toLowerCase()) && autoselectIfMatch) {
newRow = element.getRow();
hidePopup();
found = true;
break;
}
}
if (selectAuto && found && !CompareUtils.equals(newRow, selectedRow)) {
selectedRow = newRow;
SwingUtilities.invokeLater(new Runnable() {
public void run() {
ITextArticleWithCompletion.this.fireSelectionRow(ITextArticleWithCompletion.this.getSelectedRow());
}
});
}
if (!found) {
selectedRow = null;
fireSelectionRow(null);
}
}
};
worker.execute();
 
}
 
public synchronized void hidePopup() {
this.popup.setVisible(false);
}
 
private synchronized void showPopup() {
if (this.model.getSize() > 0) {
if (this.popupInvoker.isShowing())
this.popup.show(this.popupInvoker, 0, this.text.getBounds().height);
}
}
 
public void changedUpdate(DocumentEvent e) {
updateAutoCompletion(false);
this.supp.firePropertyChange("value", null, this.getText());
}
 
public void insertUpdate(DocumentEvent e) {
updateAutoCompletion(false);
this.supp.firePropertyChange("value", null, this.getText());
}
 
public void removeUpdate(DocumentEvent e) {
updateAutoCompletion(false);
this.supp.firePropertyChange("value", null, this.getText());
}
 
public SQLRowAccessor getSelectedRow() {
return this.selectedRow;
}
 
public void setSelectedRow(SQLRowAccessor row) {
this.selectedRow = row;
}
 
private void clearText() {
setText("");
}
 
public void setEditable(boolean b) {
this.text.setEditable(b);
}
 
public void setFillWithField(String s) {
this.fillWith = s;
}
 
public SQLField getFillWithField() {
return this.tableArticle.getField(fillWith);
}
 
public void selectItem(IComboSelectionItem item) {
if (!SwingUtilities.isEventDispatchThread()) {
throw new IllegalStateException("Not in Swing!");
}
if (item != null) {
if (this.fillWith != null) {
// FIXME SQL request in Swing
SQLRowAccessor row = item.getRow();
this.setText(row.getObject(this.fillWith).toString());
} else {
this.setText(item.getLabel());
}
} else {
this.clearText();
}
hidePopup();
}
 
public void setText(final String label) {
if (!SwingUtilities.isEventDispatchThread()) {
throw new IllegalStateException("Not in Swing!");
}
setCompletionEnabled(false);
 
this.text.setText(label);
if (label != null) {
this.text.setCaretPosition(label.length());
}
this.text.repaint();
setCompletionEnabled(true);
}
 
// Gestion des listeners de selection d'id
public void addSelectionListener(SelectionRowListener l) {
this.listeners.add(l);
}
 
public void removeSelectionListener(SelectionRowListener l) {
this.listeners.remove(l);
}
 
private boolean isDispatching = false;
 
private void fireSelectionRow(SQLRowAccessor row) {
if (!this.isDispatching) {
this.isDispatching = true;
for (Iterator<SelectionRowListener> iter = this.listeners.iterator(); iter.hasNext();) {
SelectionRowListener element = iter.next();
element.rowSelected(row, this);
}
this.isDispatching = false;
}
}
 
/**
* @return Returns the completionEnabled.
*/
boolean isCompletionEnabled() {
return this.completionEnabled;
}
 
/**
* @param completionEnabled The completionEnabled to set.
*/
void setCompletionEnabled(boolean completionEnabled) {
this.completionEnabled = completionEnabled;
}
 
public Object getText() {
 
return this.text.getText();
}
 
/**
* @param popupInvoker The popupInvoker to set.
*/
public void setPopupInvoker(Component popupInvoker) {
this.popupInvoker = popupInvoker;
}
 
public JTextComponent getTextComp() {
return this.text;
}
 
public JComponent getComp() {
return this;
}
 
public void setSelectionAutoEnabled(boolean b) {
this.selectAuto = b;
}
 
public void setLimitedSize(int nbChar) {
// rm previous ones
final DocumentFilterList dfl = DocumentFilterList.get((AbstractDocument) this.text.getDocument());
final Iterator<DocumentFilter> iter = dfl.getFilters().iterator();
while (iter.hasNext()) {
final DocumentFilter df = iter.next();
if (df instanceof LimitedSizeDocumentFilter)
iter.remove();
}
// add the new one
DocumentFilterList.add((AbstractDocument) this.text.getDocument(), new LimitedSizeDocumentFilter(nbChar), FilterType.SIMPLE_FILTER);
}
 
@Override
public void resetValue() {
this.setText("");
}
 
@Override
public void setValue(String val) {
this.setText(val);
}
 
@Override
public void addValueListener(PropertyChangeListener l) {
this.supp.addPropertyChangeListener(l);
}
 
@Override
public String getValue() {
return (String) this.getText();
}
 
@Override
public void rmValueListener(PropertyChangeListener l) {
this.supp.removePropertyChangeListener(l);
}
 
@Override
public void itemSelected(IComboSelectionItem item) {
if (item == null) {
fireSelectionRow(null);
} else {
final SQLRowAccessor row = item.getRow();
if (this.isLoading) {
this.rowToSelect = row;
 
} else {
if (!CompareUtils.equals(this.selectedRow, row)) {
this.setSelectedRow(row);
this.selectItem(item);
this.fireSelectionRow(row);
}
}
}
}
 
}
/trunk/OpenConcerto/src/org/openconcerto/sql/sqlobject/IComboModel.java
21,6 → 21,7
import org.openconcerto.sql.model.SQLTableEvent;
import org.openconcerto.sql.model.SQLTableModifiedListener;
import org.openconcerto.sql.request.ComboSQLRequest;
import org.openconcerto.sql.request.ComboSQLRequest.KeepMode;
import org.openconcerto.sql.view.search.SearchSpec;
import org.openconcerto.sql.view.search.SearchSpecUtils;
import org.openconcerto.ui.SwingThreadUtils;
33,6 → 34,7
import org.openconcerto.utils.checks.EmptyObj;
import org.openconcerto.utils.checks.MutableValueObject;
import org.openconcerto.utils.model.DefaultIMutableListModel;
import org.openconcerto.utils.model.ISearchable;
import org.openconcerto.utils.model.NewSelection;
 
import java.beans.PropertyChangeEvent;
58,7 → 60,7
*
* @author Sylvain CUAZ
*/
public class IComboModel extends DefaultIMutableListModel<IComboSelectionItem> implements SQLTableModifiedListener, MutableValueObject<IComboSelectionItem>, EmptyObj {
public class IComboModel extends DefaultIMutableListModel<IComboSelectionItem> implements SQLTableModifiedListener, MutableValueObject<IComboSelectionItem>, EmptyObj, ISearchable {
 
private final ComboSQLRequest req;
 
67,6 → 69,8
private boolean isADirtyDrityGirl = true;
private boolean isOnScreen = false;
private boolean sleepAllowed = true;
@GuardedBy("this")
private int requestDelay = 50;
 
// supports
private final EmptyChangeSupport emptySupp;
192,6 → 196,7
}
 
void setRunning(final boolean b) {
assert SwingUtilities.isEventDispatchThread();
if (this.running != b) {
this.running = b;
if (this.running) {
245,7 → 250,21
return this.sleepAllowed;
}
 
public synchronized final int getRequestDelay() {
return this.requestDelay;
}
 
/**
* Set the delay before the request is executed. I.e. if two {@link #fillCombo()} are less than
* <code>delay</code> apart the first one won't be executed.
*
* @param delay the delay in milliseconds.
*/
public synchronized final void setRequestDelay(final int delay) {
this.requestDelay = delay;
}
 
/**
* Reload this combo. This method is thread-safe.
*/
public synchronized final void fillCombo() {
295,6 → 314,7
// and thus done() and r might never be called
if (r != null)
this.runnables.add(r);
final int delay = this.getRequestDelay();
// copy the current search, if it changes fillCombo() will be called
final SearchSpec search = this.getSearch();
// commencer l'update après, sinon modeToSelect == 0
303,7 → 323,7
@Override
protected List<IComboSelectionItem> doInBackground() throws InterruptedException {
// attends 1 peu pour voir si on va pas être annulé
Thread.sleep(50);
Thread.sleep(delay);
return SearchSpecUtils.filter(IComboModel.this.req.getComboItems(readCache), search);
}
 
463,8 → 483,11
return this.getSelectedValue();
else if (this.getWantedID() == SQLRow.NONEXISTANT_ID)
return null;
else if (this.getRequest().getKeepMode() == KeepMode.NONE)
return new IComboSelectionItem(getWantedID(), null);
else
return new IComboSelectionItem(getWantedID(), null);
// no point in passing an SQLRowValues as the graph would be limited to just this row
return new IComboSelectionItem(new SQLRow(this.getForeignTable(), getWantedID()), null);
}
 
/**
598,7 → 621,7
error = " archivée";
else
error = " existe mais est non atteignable: " + row.findDistantArchived(2);
newItem = new IComboSelectionItem(id, "ERREUR !!! " + row + error);
newItem = new IComboSelectionItem(row, "ERREUR !!! " + row + error);
newItem.setFlag(IComboSelectionItem.ERROR_FLAG);
}
this.addItem(newItem);
755,4 → 778,24
assert SwingUtilities.isEventDispatchThread();
return this.updating;
}
 
@Override
public boolean isSearchable() {
return !this.getRequest().getSearchFields().isEmpty();
}
 
@Override
public boolean setSearch(String s, Runnable r) {
if (this.getRequest().setSearch(s)) {
if (r != null) {
synchronized (this) {
this.runnables.add(r);
}
}
return true;
} else {
SwingUtilities.invokeLater(r);
return false;
}
}
}
/trunk/OpenConcerto/src/org/openconcerto/sql/sqlobject/ITextArticleWithCompletionCellEditor.java
New file
0,0 → 1,66
/*
* 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.sqlobject;
 
import org.openconcerto.sql.model.SQLRowAccessor;
import org.openconcerto.sql.model.SQLTable;
 
import java.awt.Component;
 
import javax.swing.AbstractCellEditor;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.TableCellEditor;
 
public class ITextArticleWithCompletionCellEditor extends AbstractCellEditor implements TableCellEditor {
 
private final ITextArticleWithCompletion text;
 
public ITextArticleWithCompletionCellEditor(SQLTable tableArticle, SQLTable tableARticleFournisseur) {
this.text = new ITextArticleWithCompletion(tableArticle, tableARticleFournisseur);
}
 
@Override
public Object getCellEditorValue() {
return this.text.getText();
}
 
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
 
if (value != null) {
this.text.setText((String) value);
} else {
this.text.setText("");
}
 
Runnable r = new Runnable() {
 
public void run() {
text.getTextComp().grabFocus();
}
};
SwingUtilities.invokeLater(r);
 
return this.text;
}
 
public void addSelectionListener(SelectionRowListener l) {
this.text.addSelectionListener(l);
}
 
public SQLRowAccessor getComboSelectedRow() {
return this.text.getSelectedRow();
}
}
/trunk/OpenConcerto/src/org/openconcerto/sql/sqlobject/SQLSearchableTextCombo.java
15,6 → 15,8
 
import org.openconcerto.sql.model.SQLField;
import org.openconcerto.sql.request.SQLRowItemView;
import org.openconcerto.sql.sqlobject.SQLTextCombo.AbstractComboCacheSQL;
import org.openconcerto.sql.sqlobject.SQLTextCombo.ITextComboCacheExistingValues;
import org.openconcerto.sql.sqlobject.SQLTextCombo.ITextComboCacheSQL;
import org.openconcerto.sql.sqlobject.itemview.RowItemViewComponent;
import org.openconcerto.ui.component.ComboLockedMode;
28,6 → 30,8
*/
public class SQLSearchableTextCombo extends ISearchableTextCombo implements RowItemViewComponent {
 
private boolean useFieldValues = false;
 
public SQLSearchableTextCombo() {
this(ComboLockedMode.UNLOCKED);
}
52,11 → 56,40
super(mode, rows, columns, textArea);
}
 
public final boolean getUseFieldValues() {
return this.useFieldValues;
}
 
/**
* Whether to use the existing values for the field or values listed in the
* {@link SQLTextCombo#getTableName() completion} table.
*
* @param useFieldValues <code>true</code> to use existing values (thus incompatible with a
* mutable list), <code>false</code> to use the completion table.
* @return this.
* @throws IllegalStateException if the cache is already
* {@link #initCache(org.openconcerto.utils.model.IListModel) initialized} or if the mode
* {@link ComboLockedMode#isListMutable()} and <code>useFieldValues</code> is
* <code>true</code>.
*/
public final SQLSearchableTextCombo setUseFieldValues(final boolean useFieldValues) {
if (this.getCache() != null)
throw new IllegalStateException("Cache already created");
if (this.useFieldValues != useFieldValues) {
if (useFieldValues && this.getMode().isListMutable())
throw new IllegalStateException("Cannot modify list when using field values");
this.useFieldValues = useFieldValues;
}
return this;
}
 
@Override
public void init(SQLRowItemView v) {
if (this.getCache() == null)
this.initCache(new ISQLListModel(v.getField()).load());
if (this.getCache() == null) {
final AbstractComboCacheSQL cache = this.useFieldValues ? new ITextComboCacheExistingValues(v.getField()) : new ITextComboCacheSQL(v.getField());
this.initCache(new ISQLListModel(cache).load());
}
}
 
/**
* Load <code>cache</code> and only afterwards call
74,7 → 107,7
this(new ITextComboCacheSQL(f));
}
 
public ISQLListModel(final ITextComboCacheSQL c) {
public ISQLListModel(final AbstractComboCacheSQL c) {
super(c);
}
}
/trunk/OpenConcerto/src/org/openconcerto/sql/Configuration.java
32,8 → 32,11
import java.io.File;
import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
import net.jcip.annotations.GuardedBy;
 
/**
* Regroupe les objets nécessaires au framework.
*
59,7 → 62,8
Configuration.instance = instance;
}
 
private Executor nonInteractiveSQLExecutor;
@GuardedBy("this")
private ExecutorService nonInteractiveSQLExecutor;
 
public abstract ShowAs getShowAs();
 
160,7 → 164,13
/**
* Signal that this conf will not be used anymore.
*/
public abstract void destroy();
public void destroy() {
synchronized (this) {
if (this.nonInteractiveSQLExecutor != null) {
this.nonInteractiveSQLExecutor.shutdown();
}
}
}
 
/**
* An executor that should be used for background SQL requests. It can be used to limit the
169,7 → 179,7
*
* @return a SQL executor.
*/
public Executor getNonInteractiveSQLExecutor() {
public synchronized final Executor getNonInteractiveSQLExecutor() {
if (this.nonInteractiveSQLExecutor == null) {
this.nonInteractiveSQLExecutor = createNonInteractiveSQLExecutor();
}
176,7 → 186,7
return this.nonInteractiveSQLExecutor;
}
 
protected Executor createNonInteractiveSQLExecutor() {
protected ExecutorService createNonInteractiveSQLExecutor() {
return Executors.newFixedThreadPool(2);
}
}
/trunk/OpenConcerto/src/org/openconcerto/sql/request/SQLRowView.java
30,6 → 30,7
import java.beans.PropertyChangeSupport;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
324,10 → 325,15
return new LinkedHashSet<SQLRowItemView>(this.getViewsFast());
}
 
public Map<String, SQLRowItemView> getViewsMap() {
return Collections.unmodifiableMap(this.views);
}
 
private final Collection<SQLRowItemView> getViewsFast() {
return this.viewsOrdered;
}
 
@Override
public String toString() {
return this.getClass() + " with " + this.getViewsFast();
}
/trunk/OpenConcerto/src/org/openconcerto/sql/request/ComboSQLRequest.java
32,7 → 32,6
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.RTInterruptedException;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.utils.Tuple3;
import org.openconcerto.utils.cache.CacheResult;
import org.openconcerto.utils.cc.IClosure;
import org.openconcerto.utils.cc.IPredicate;
42,6 → 41,7
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
 
// final: use setSelectTransf()
50,9 → 50,15
static private final SQLCache<CacheKey, List<IComboSelectionItem>> cache = new SQLCache<CacheKey, List<IComboSelectionItem>>(60, -1, "items of " + ComboSQLRequest.class);
 
// encapsulate all values that can change the result
private static final class CacheKey extends Tuple3<SQLRowValuesListFetcher, IClosure<IComboSelectionItem>, String> {
public CacheKey(SQLRowValuesListFetcher a, String fieldSeparator, String undefLabel, IClosure<IComboSelectionItem> c) {
super(a, c, fieldSeparator + undefLabel);
protected static final class CacheKey extends LinkedList<Object> {
public CacheKey(SQLRowValuesListFetcher a, String fieldSeparator, String undefLabel, IClosure<IComboSelectionItem> c, KeepMode keepRows, Comparator<? super IComboSelectionItem> itemsOrder) {
super();
this.add(a);
this.add(c);
this.add(fieldSeparator);
this.add(undefLabel);
this.add(keepRows);
this.add(itemsOrder);
}
};
 
202,7 → 208,7
// and that will cause the cache to fail
final SQLRowValuesListFetcher comboSelect = this.getFetcher(null).freeze();
 
final CacheKey cacheKey = new CacheKey(comboSelect, this.fieldSeparator, this.undefLabel, this.customizeItem);
final CacheKey cacheKey = getCacheKey(comboSelect);
if (readCache) {
final CacheResult<List<IComboSelectionItem>> l = cache.check(cacheKey);
if (l.getState() == CacheResult.State.INTERRUPTED)
222,7 → 228,7
if (this.itemsOrder != null)
Collections.sort(result, this.itemsOrder);
 
cache.put(cacheKey, result, this.getTables());
cache.put(cacheKey, result, comboSelect.getGraph().getGraph().getTables());
 
return result;
} catch (RuntimeException exn) {
232,6 → 238,14
}
}
 
protected final CacheKey getCacheKey() {
return getCacheKey(this.getFetcher(null).freeze());
}
 
private final CacheKey getCacheKey(final SQLRowValuesListFetcher comboSelect) {
return new CacheKey(comboSelect, this.fieldSeparator, this.undefLabel, this.customizeItem, this.keepRows, this.itemsOrder);
}
 
@Override
protected final SQLSelect transformSelect(SQLSelect sel) {
sel.setExcludeUndefined(this.undefLabel == null, getPrimaryTable());
297,9 → 311,9
}
});
final IComboSelectionItem res;
if (this.keepRows == KeepMode.GRAPH)
if (this.getKeepMode() == KeepMode.GRAPH)
res = new IComboSelectionItem(rs, desc);
else if (this.keepRows == KeepMode.ROW)
else if (this.getKeepMode() == KeepMode.ROW)
res = new IComboSelectionItem(rs.asRow(), desc);
else
res = new IComboSelectionItem(rs.getID(), desc);
353,6 → 367,10
return SEP_CHILD + this.fieldSeparator;
}
 
public final KeepMode getKeepMode() {
return this.keepRows;
}
 
/**
* Whether {@link IComboSelectionItem items} retain their rows.
*
/trunk/OpenConcerto/src/org/openconcerto/sql/request/UpdateBuilder.java
120,7 → 120,7
* @param value the SQL, e.g. a quoted field from the joined table or an arbitrary expression.
* @return this.
* @see #setFromVirtualJoinField(String, String, String)
* @see #addVirtualJoin(TableRef, String)
* @see #addVirtualJoin(String, String, boolean, String, String, boolean)
*/
public final UpdateBuilder setFromVirtualJoin(final String field, final String joinAlias, final String value) {
final String val;
/trunk/OpenConcerto/src/org/openconcerto/sql/request/BaseFillSQLRequest.java
14,23 → 14,37
package org.openconcerto.sql.request;
 
import org.openconcerto.sql.FieldExpander;
import org.openconcerto.sql.model.FieldRef;
import org.openconcerto.sql.model.SQLField;
import org.openconcerto.sql.model.SQLRowValues;
import org.openconcerto.sql.model.SQLRowValuesListFetcher;
import org.openconcerto.sql.model.SQLSearchMode;
import org.openconcerto.sql.model.SQLSelect;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.Where;
import org.openconcerto.sql.model.graph.Path;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.cc.ITransformer;
 
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;
 
import net.jcip.annotations.GuardedBy;
 
public abstract class BaseFillSQLRequest extends BaseSQLRequest {
 
private final static Pattern QUERY_SPLIT_PATTERN = Pattern.compile("\\s+");
private static boolean DEFAULT_SELECT_LOCK = true;
 
/**
56,6 → 70,10
 
private final SQLTable primaryTable;
private Where where;
@GuardedBy("this")
private Map<SQLField, SQLSearchMode> searchFields;
@GuardedBy("this")
private List<String> searchQuery;
private ITransformer<SQLSelect, SQLSelect> selTransf;
private boolean lockSelect;
 
70,6 → 88,8
throw new NullPointerException();
this.primaryTable = primaryTable;
this.where = w;
this.searchFields = Collections.emptyMap();
this.searchQuery = Collections.emptyList();
this.selTransf = null;
this.lockSelect = getDefaultLockSelect();
this.graph = null;
80,6 → 100,8
super();
this.primaryTable = req.getPrimaryTable();
this.where = req.where;
this.searchFields = req.searchFields;
this.searchQuery = req.searchQuery;
this.selTransf = req.selTransf;
this.lockSelect = req.lockSelect;
// TODO copy
180,6 → 202,79
return this.where;
}
 
/**
* Whether this request is searchable.
*
* @param b <code>true</code> if the {@link #getFields() local fields} should be used,
* <code>false</code> to not be searchable.
*/
public final void setSearchable(final boolean b) {
this.setSearchFields(b ? this.getFields() : Collections.<SQLField> emptyList());
}
 
/**
* Set the fields used to search.
*
* @param searchFields only rows with these fields containing the terms will match.
* @see #setSearch(String)
*/
public final void setSearchFields(final Collection<SQLField> searchFields) {
this.setSearchFields(CollectionUtils.<SQLField, SQLSearchMode> createMap(searchFields));
}
 
/**
* Set the fields used to search.
*
* @param searchFields for each field to search, how to match.
* @see #setSearch(String)
*/
public final void setSearchFields(Map<SQLField, SQLSearchMode> searchFields) {
searchFields = new HashMap<SQLField, SQLSearchMode>(searchFields);
final Iterator<Entry<SQLField, SQLSearchMode>> iter = searchFields.entrySet().iterator();
while (iter.hasNext()) {
final Entry<SQLField, SQLSearchMode> e = iter.next();
if (!String.class.isAssignableFrom(e.getKey().getType().getJavaType())) {
iter.remove();
} else if (e.getValue() == null) {
e.setValue(SQLSearchMode.CONTAINS);
}
}
searchFields = Collections.unmodifiableMap(searchFields);
synchronized (this) {
this.searchFields = searchFields;
}
fireWhereChange();
}
 
public Map<SQLField, SQLSearchMode> getSearchFields() {
synchronized (this) {
return this.searchFields;
}
}
 
/**
* Set the search query. The query will be used to match rows using
* {@link #setSearchFields(Map)}. I.e. if there's no field set, this method won't have any
* effect.
*
* @param s the search query.
* @return <code>true</code> if the request changed.
*/
public boolean setSearch(String s) {
// no need to trim() since trailing empty strings are not returned
final List<String> split = Arrays.asList(QUERY_SPLIT_PATTERN.split(s));
synchronized (this) {
if (!split.equals(this.searchQuery)) {
this.searchQuery = split;
if (!this.getSearchFields().isEmpty()) {
this.fireWhereChange();
return true;
}
}
return false;
}
}
 
public final void setLockSelect(boolean lockSelect) {
this.lockSelect = lockSelect;
}
198,9 → 293,43
protected abstract Collection<SQLField> getFields();
 
protected SQLSelect transformSelect(final SQLSelect sel) {
final Map<SQLField, SQLSearchMode> searchFields;
final List<String> searchQuery;
synchronized (this) {
searchFields = this.getSearchFields();
searchQuery = this.searchQuery;
}
final Where w;
final Set<String> matchScore = new HashSet<String>();
if (!searchFields.isEmpty()) {
Where where = null;
for (final String searchTerm : searchQuery) {
Where termWhere = null;
for (final FieldRef selF : sel.getSelectFields()) {
final SQLSearchMode mode = searchFields.get(selF.getField());
if (mode != null) {
termWhere = Where.createRaw(createWhere(selF, mode, searchTerm)).or(termWhere);
if (!mode.equals(SQLSearchMode.EQUALS))
matchScore.add("case when " + createWhere(selF, SQLSearchMode.EQUALS, searchTerm) + " then 1 else 0 end");
}
}
where = Where.and(termWhere, where);
}
w = where;
} else {
w = null;
}
sel.andWhere(w);
if (!matchScore.isEmpty())
sel.getOrder().add(0, CollectionUtils.join(matchScore, " + ") + " DESC");
 
return this.selTransf == null ? sel : this.selTransf.transformChecked(sel);
}
 
protected String createWhere(final FieldRef selF, final SQLSearchMode mode, final String searchQuery) {
return "lower(" + selF.getFieldRef() + ") " + mode.generateSQL(selF.getField().getDBRoot(), searchQuery.toLowerCase());
}
 
public final ITransformer<SQLSelect, SQLSelect> getSelectTransf() {
return this.selTransf;
}
/trunk/OpenConcerto/src/org/openconcerto/sql/request/SQLFieldTranslator.java
52,6 → 52,9
import java.util.Set;
import java.util.prefs.Preferences;
 
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;
 
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
63,6 → 66,7
* @author ilm 22 nov. 2004
* @see #getDescFor(SQLTable, String)
*/
@ThreadSafe
public class SQLFieldTranslator {
 
// OK since RowItemDesc is immutable
101,7 → 105,7
createValueT.addUniqueConstraint("uniq", Arrays.asList(ELEM_FIELDNAME, COMP_FIELDNAME, ITEM_FIELDNAME));
createValueT.addVarCharColumn(LABEL_FIELDNAME, 256);
createValueT.addVarCharColumn(COL_TITLE_FIELDNAME, 256);
createValueT.addVarCharColumn(DOC_FIELDNAME, Math.min(8192, Preferences.MAX_VALUE_LENGTH));
createValueT.addVarCharColumn(DOC_FIELDNAME, Preferences.MAX_VALUE_LENGTH, true);
root.createTable(createValueT);
}
return root.getTable(METADATA_TABLENAME);
133,9 → 137,11
// Instance members
 
// { SQLTable -> { compCode, variant, item -> RowItemDesc }}
@GuardedBy("this")
private final Map<SQLTable, Map<List<String>, RowItemDesc>> translation;
private final SQLTable table;
private final SQLElementDirectory dir;
@GuardedBy("this")
private final Set<String> unknownCodes;
 
{
174,7 → 180,11
 
@Override
public void elementAdded(SQLElement elem) {
if (SQLFieldTranslator.this.unknownCodes.remove(elem.getCode())) {
final boolean isUnknown;
synchronized (SQLFieldTranslator.this) {
isUnknown = SQLFieldTranslator.this.unknownCodes.contains(elem.getCode());
}
if (isUnknown) {
fetch(Collections.singleton(elem.getCode()));
}
}
191,8 → 201,26
* @param o another SQLFieldTranslator to add.
*/
public void putAll(SQLFieldTranslator o) {
if (o == this)
return;
final int thisHash = System.identityHashCode(this);
final int oHash = System.identityHashCode(o);
final SQLFieldTranslator o1, o2;
if (thisHash < oHash) {
o1 = this;
o2 = o;
} else if (thisHash > oHash) {
o1 = o;
o2 = this;
} else {
throw new IllegalStateException("Hash equal");
}
synchronized (o1) {
synchronized (o2) {
CollectionUtils.addIfNotPresent(this.translation, o.translation);
}
}
}
 
public void load(DBRoot b, File file) {
try {
307,7 → 335,7
}
 
private List<SQLRow> fetchOnly(final SQLTable table, final Where w) {
return SQLRowListRSH.execute(new SQLSelect(table.getBase()).addSelectStar(table).setWhere(w));
return SQLRowListRSH.execute(new SQLSelect().addSelectStar(table).setWhere(w));
}
 
private void fetchAndPut(final SQLTable table, final Set<String> codes) {
322,7 → 350,6
}
for (final SQLRow r : fetchOnly(table, w)) {
final String elementCode = r.getString(ELEM_FIELDNAME);
if (!this.unknownCodes.contains(elementCode)) {
// needed since tables can be loaded at any time in SQLElementDirectory
// MAYBE use code as the map key instead of SQLTable
final SQLElement elem = this.dir.getElementForCode(elementCode);
330,8 → 357,12
final String componentCode = r.getString(COMP_FIELDNAME);
final String item = r.getString(ITEM_FIELDNAME);
final RowItemDesc desc = new RowItemDesc(r.getString(LABEL_FIELDNAME), r.getString(COL_TITLE_FIELDNAME), r.getString(DOC_FIELDNAME));
synchronized (this) {
putTranslation(elem.getTable(), componentCode, DB_VARIANT, item, desc);
this.unknownCodes.remove(elementCode);
}
} else {
synchronized (this) {
this.unknownCodes.add(elementCode);
}
}
338,7 → 369,7
}
}
 
private final Map<List<String>, RowItemDesc> getMap(final SQLTable t) {
private synchronized final Map<List<String>, RowItemDesc> getMap(final SQLTable t) {
Map<List<String>, RowItemDesc> elemMap = this.translation.get(t);
if (elemMap == null) {
elemMap = new HashMap<List<String>, RowItemDesc>();
347,7 → 378,7
return elemMap;
}
 
private final void putTranslation(SQLTable t, String compCode, String variant, String item, RowItemDesc desc) {
private synchronized final void putTranslation(SQLTable t, String compCode, String variant, String item, RowItemDesc desc) {
if (t == null)
throw new IllegalArgumentException("Table cannot be null");
// needed by remove()
356,7 → 387,7
this.getMap(t).put(Arrays.asList(compCode, variant, item), desc);
}
 
private final void removeTranslation(SQLTable t, String compCode, String variant, String name) {
private synchronized final void removeTranslation(SQLTable t, String compCode, String variant, String name) {
// null means match everything, OK since we test in putTranslation() that we don't contain
// null values
if (t == null) {
368,7 → 399,7
}
}
 
private void removeTranslation(Map<List<String>, RowItemDesc> m, String compCode, String variant, String name) {
private synchronized void removeTranslation(Map<List<String>, RowItemDesc> m, String compCode, String variant, String name) {
if (m == null)
return;
 
386,7 → 417,7
}
}
 
private final RowItemDesc getTranslation(SQLTable t, String compCode, String variant, String item) {
private synchronized final RowItemDesc getTranslation(SQLTable t, String compCode, String variant, String item) {
return this.getMap(t).get(Arrays.asList(compCode, variant, item));
}
 
/trunk/OpenConcerto/src/org/openconcerto/sql/element/DeletionMode.java
File deleted
/trunk/OpenConcerto/src/org/openconcerto/sql/element/BaseSQLComponent.java
108,7 → 108,8
protected static final String SEP = "noseparator";
 
/**
* Syntactic sugar for {@link BaseSQLComponent#createRowItemView(String, Class, ITransformer)}.
* Syntactic sugar for
* {@link BaseSQLComponent#createSimpleRowItemView(String, Class, ITransformer)}.
*
* @author Sylvain CUAZ
* @param <T> type parameter
/trunk/OpenConcerto/src/org/openconcerto/sql/element/ConfSQLElement.java
15,6 → 15,7
 
import org.openconcerto.sql.Configuration;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.utils.i18n.Phrase;
 
import java.util.Collections;
import java.util.List;
40,17 → 41,29
}
 
public ConfSQLElement(String tableName) {
this(Configuration.getInstance(), tableName);
this(tableName, null);
}
 
public ConfSQLElement(Configuration conf, String tableName) {
this(conf.getRoot().findTable(tableName));
this(conf, tableName, null);
}
 
public ConfSQLElement(SQLTable table) {
super(table);
this(table, null);
}
 
public ConfSQLElement(String tableName, final Phrase name) {
this(Configuration.getInstance(), tableName, name);
}
 
public ConfSQLElement(Configuration conf, String tableName, final Phrase name) {
this(conf.getRoot().findTable(tableName), name);
}
 
public ConfSQLElement(final SQLTable primaryTable, final Phrase name) {
super(primaryTable, name);
}
 
@Override
protected List<String> getComboFields() {
return Collections.emptyList();
/trunk/OpenConcerto/src/org/openconcerto/sql/element/SQLElementRowR.java
15,7 → 15,6
 
import org.openconcerto.sql.model.SQLRow;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.utils.CollectionMap;
 
import java.util.HashMap;
import java.util.Iterator;
62,13 → 61,13
private boolean equalsRec(SQLElementRowR o, Map<SQLRow, SQLRow> copies) {
if (!SQLElementRow.equals(this.getRow(), o.getRow()))
return false;
final CollectionMap<SQLTable, SQLRow> children1 = this.getElem().getChildrenRows(this.getRow());
final CollectionMap<SQLTable, SQLRow> children2 = this.getElem().getChildrenRows(o.getRow());
final Map<SQLTable, List<SQLRow>> children1 = this.getElem().getChildrenRows(this.getRow());
final Map<SQLTable, List<SQLRow>> children2 = this.getElem().getChildrenRows(o.getRow());
if (!children1.keySet().equals(children2.keySet()))
return false;
for (final SQLTable childT : children1.keySet()) {
final List<SQLRow> l1 = (List<SQLRow>) children1.getNonNull(childT);
final List<SQLRow> l2 = (List<SQLRow>) children2.getNonNull(childT);
final List<SQLRow> l1 = children1.get(childT);
final List<SQLRow> l2 = children2.get(childT);
if (l1.size() != l2.size())
return false;
 
/trunk/OpenConcerto/src/org/openconcerto/sql/element/SQLElementDirectory.java
18,8 → 18,8
import org.openconcerto.sql.model.DBStructureItemNotFound;
import org.openconcerto.sql.model.SQLName;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.utils.CollectionMap;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.SetMap;
import org.openconcerto.utils.cc.ITransformer;
import org.openconcerto.utils.i18n.LocalizedInstances;
import org.openconcerto.utils.i18n.Phrase;
32,7 → 32,6
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
71,9 → 70,9
};
 
private final Map<SQLTable, SQLElement> elements;
private final CollectionMap<String, SQLTable> tableNames;
private final CollectionMap<String, SQLTable> byCode;
private final CollectionMap<Class<? extends SQLElement>, SQLTable> byClass;
private final SetMap<String, SQLTable> tableNames;
private final SetMap<String, SQLTable> byCode;
private final SetMap<Class<? extends SQLElement>, SQLTable> byClass;
private final List<DirectoryListener> listeners;
 
private String phrasesPkgName;
83,9 → 82,9
this.elements = new HashMap<SQLTable, SQLElement>();
// to mimic elements behaviour, if we add twice the same table
// the second one should replace the first one
this.tableNames = new CollectionMap<String, SQLTable>(HashSet.class);
this.byCode = new CollectionMap<String, SQLTable>(HashSet.class);
this.byClass = new CollectionMap<Class<? extends SQLElement>, SQLTable>(HashSet.class);
this.tableNames = new SetMap<String, SQLTable>();
this.byCode = new SetMap<String, SQLTable>();
this.byClass = new SetMap<Class<? extends SQLElement>, SQLTable>();
 
this.listeners = new ArrayList<DirectoryListener>();
 
93,7 → 92,7
this.elementNames = new HashMap<String, SQLElementNames>();
}
 
private static <K> SQLTable getSoleTable(CollectionMap<K, SQLTable> m, K key) throws IllegalArgumentException {
private static <K> SQLTable getSoleTable(SetMap<K, SQLTable> m, K key) throws IllegalArgumentException {
final Collection<SQLTable> res = m.getNonNull(key);
if (res.size() > 1)
throw new IllegalArgumentException(key + " is not unique: " + CollectionUtils.join(res, ",", new ITransformer<SQLTable, SQLName>() {
141,9 → 140,9
public synchronized final SQLElement addSQLElement(SQLElement elem) {
final SQLElement res = this.removeSQLElement(elem.getTable());
this.elements.put(elem.getTable(), elem);
this.tableNames.put(elem.getTable().getName(), elem.getTable());
this.byCode.put(elem.getCode(), elem.getTable());
this.byClass.put(elem.getClass(), elem.getTable());
this.tableNames.add(elem.getTable().getName(), elem.getTable());
this.byCode.add(elem.getCode(), elem.getTable());
this.byClass.add(elem.getClass(), elem.getTable());
for (final DirectoryListener dl : this.listeners) {
dl.elementAdded(elem);
}
/trunk/OpenConcerto/src/org/openconcerto/sql/element/GroupSQLComponent.java
36,8 → 36,10
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
 
63,6 → 65,7
private boolean tabGroup;
private int tabDepth;
private JTabbedPane pane;
private final List<String> tabsGroupIDs = new ArrayList<String>();
 
public GroupSQLComponent(final SQLElement element, final Group group) {
super(element);
69,6 → 72,10
this.group = group;
}
 
protected final Group getGroup() {
return this.group;
}
 
public void startTabGroupAfter(String id) {
startTabAfter = id;
}
112,6 → 119,7
label = id;
}
this.pane.addTab(label, panel);
this.tabsGroupIDs.add(currentGroup.getId());
} else {
if (size.showLabel() && getLabel(id) != null) {
x = 0;
238,6 → 246,29
 
}
 
public final void setTabEnabledAt(final String groupID, final boolean enabled) {
this.pane.setEnabledAt(this.tabsGroupIDs.indexOf(groupID), enabled);
}
 
public final boolean isTabEnabledAt(final String groupID) {
return this.pane.isEnabledAt(this.tabsGroupIDs.indexOf(groupID));
}
 
public final void selectTabEnabled() {
final int index = this.pane.getSelectedIndex();
if (!this.pane.isEnabledAt(index)) {
final int count = this.pane.getTabCount();
// 1 since index is disabled
for (int i = 1; i < count; i++) {
final int mod = (index + i) % count;
if (this.pane.isEnabledAt(mod)) {
this.pane.setSelectedIndex(mod);
return;
}
}
}
}
 
@Override
public Component addView(JComponent comp, String id) {
final FieldMapper fieldMapper = PropsConfiguration.getInstance().getFieldMapper();
/trunk/OpenConcerto/src/org/openconcerto/sql/element/ElementSQLObject.java
40,7 → 40,8
*/
public abstract class ElementSQLObject extends BaseSQLObject implements SQLForeignRowItemView {
 
protected boolean required;
private boolean required;
private boolean createdUIVisible = true;
private final SQLComponent parent;
private final SQLComponent comp;
 
95,9 → 96,32
this.required = required;
if (this.required)
this.setCreated(true);
uiChanged();
}
 
protected final boolean deleteAllowed() {
return !this.required;
}
 
/**
* Whether the UI to create and delete are visible. If the UI isn't visible, the creation must
* be managed by {@link #setCreated(boolean)}.
*
* @param b <code>true</code> if the buttons should be visible, <code>false</code> to hide them.
*/
public final void setCreatedUIVisible(boolean b) {
this.createdUIVisible = b;
uiChanged();
}
 
public final boolean isCreatedUIVisible() {
return this.createdUIVisible;
}
 
protected void uiChanged() {
}
 
/**
* Called in the constructor, before setCreatePanel() or setEditPanel(). Should be used to
* initialize ui components such as buttons.
*/
139,9 → 163,10
return this.created;
}
 
@Override
public void setValue(SQLRowAccessor r) {
// a row with no ID is displayable but not the undefined row
final boolean displayableRow = r != null && r.getID() != r.getTable().getUndefinedID();
final boolean displayableRow = r != null && !r.isUndefined();
if (displayableRow) {
// d'abord setCreated, car il fait un reset()
this.setCreated(true);
148,7 → 173,7
this.setCurrentID(r);
} else {
// reset
this.setCreated(this.required);
this.setCreated(!this.deleteAllowed());
this.setCurrentID(null);
}
compChanged();
215,8 → 240,9
return this.comp;
}
 
@Override
public String toString() {
return "ElementSQLObject on " + this.getField() + " created: " + this.isCreated() + " id: " + this.getCurrentID();
return "ElementSQLObject on " + this.getFields() + " created: " + this.isCreated() + " id: " + this.getCurrentID();
}
 
/**
/trunk/OpenConcerto/src/org/openconcerto/sql/element/TreesOfSQLRows.java
14,20 → 14,23
package org.openconcerto.sql.element;
 
import org.openconcerto.sql.Configuration;
import org.openconcerto.sql.TM;
import org.openconcerto.sql.element.SQLElement.ReferenceAction;
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.SQLRowValuesCluster;
import org.openconcerto.sql.model.SQLRowValuesListFetcher;
import org.openconcerto.sql.model.SQLSelect;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.SQLTable.VirtualFields;
import org.openconcerto.sql.model.Where;
import org.openconcerto.sql.model.SQLRowValuesCluster.State;
import org.openconcerto.sql.model.graph.Link;
import org.openconcerto.sql.model.graph.Link.Direction;
import org.openconcerto.utils.CollectionMap;
import org.openconcerto.utils.RecursionType;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.ListMap;
import org.openconcerto.utils.SetMap;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.utils.cc.ITransformer;
 
36,15 → 39,17
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.Map.Entry;
 
/**
* Cache several trees of rows (a row and its descendants).
65,24 → 70,24
final String fieldLabel = Configuration.getTranslator(fk.getSource()).getLabelFor(fk.getLabel());
final String fieldS = fieldLabel != null ? fieldLabel : fk.getLabel().getName();
// la tâche du 26/05 ne peut perdre son champ UTILISATEUR
return rowDesc + " ne peut perdre son champ " + fieldS;
return TM.getTM().trM("sqlElement.linkCantBeCut", CollectionUtils.createMap("row", refVals.asRow(), "rowDesc", rowDesc, "fieldLabel", fieldS));
}
 
private final SQLElement elem;
private final Map<SQLRow, SQLRowValues> trees;
private CollectionMap<SQLField, SQLRow> externReferences;
private SetMap<SQLField, SQLRow> externReferences;
 
public TreesOfSQLRows(final SQLElement elem, SQLRow row) {
this(elem, Collections.singleton(row));
}
 
public TreesOfSQLRows(final SQLElement elem, final Collection<SQLRow> rows) {
public TreesOfSQLRows(final SQLElement elem, final Collection<? extends SQLRowAccessor> rows) {
super();
this.elem = elem;
this.trees = new HashMap<SQLRow, SQLRowValues>(rows.size());
for (final SQLRow r : rows) {
for (final SQLRowAccessor r : rows) {
this.elem.check(r);
this.trees.put(r, null);
this.trees.put(r.asRow(), null);
}
this.externReferences = null;
}
95,40 → 100,76
return this.trees.keySet();
}
 
public final Set<SQLRowValues> getTrees() throws SQLException {
final Set<SQLRowValues> res = new HashSet<SQLRowValues>();
for (final SQLRow r : this.getRows()) {
res.add(this.getTree(r));
public final Map<SQLRow, SQLRowValues> getTrees() throws SQLException {
if (this.externReferences == null) {
final Tuple2<Map<SQLRow, SQLRowValues>, SetMap<SQLField, SQLRow>> expand = this.expand();
assert expand.get0().keySet().equals(this.trees.keySet());
this.trees.putAll(expand.get0());
this.externReferences = expand.get1();
}
return res;
return Collections.unmodifiableMap(this.trees);
}
 
private final SQLRowValues getTree(SQLRow r) throws SQLException {
if (!this.trees.containsKey(r))
throw new IllegalArgumentException();
SQLRowValues res = this.trees.get(r);
if (res == null) {
final Tuple2<SQLRowValues, CollectionMap<SQLField, SQLRow>> expand = this.expand(r);
res = expand.get0();
this.trees.put(r, res);
if (this.externReferences == null)
// allow to specify the attributes of the map only once
this.externReferences = expand.get1();
else
this.externReferences.merge(expand.get1());
public final Set<SQLRowValuesCluster> getClusters() throws SQLException {
final Set<SQLRowValuesCluster> res = Collections.newSetFromMap(new IdentityHashMap<SQLRowValuesCluster, Boolean>());
for (final SQLRowValues r : this.getTrees().values()) {
// trees can be linked together
res.add(r.getGraph());
}
return res;
}
 
private final Tuple2<SQLRowValues, CollectionMap<SQLField, SQLRow>> expand(SQLRow r) throws SQLException {
final SQLRowValues vals = r.createUpdateRow();
final Set<SQLRow> hasBeen = new HashSet<SQLRow>();
hasBeen.add(r);
final CollectionMap<SQLField, SQLRow> toCut = new CollectionMap<SQLField, SQLRow>(new HashSet<SQLRow>());
expand(vals.getTable(), Collections.singletonMap(vals.getID(), vals), hasBeen, toCut);
return Tuple2.create(vals, toCut);
private final Tuple2<Map<SQLRow, SQLRowValues>, SetMap<SQLField, SQLRow>> expand() throws SQLException {
final Map<Integer, SQLRowValues> valsMap = new HashMap<Integer, SQLRowValues>();
final Map<SQLRow, SQLRowValues> hasBeen = new HashMap<SQLRow, SQLRowValues>();
final SetMap<SQLField, SQLRow> toCut = new SetMap<SQLField, SQLRow>();
 
// fetch privates of root rows
final SQLRowValues privateGraph = this.getElem().getPrivateGraph(EnumSet.of(VirtualFields.FOREIGN_KEYS));
final Privates privates;
if (privateGraph.getGraph().size() > 1) {
privates = new Privates(hasBeen, toCut);
final Set<Number> ids = new HashSet<Number>();
for (final SQLRow r : this.getRows()) {
ids.add(r.getIDNumber());
}
final SQLRowValuesListFetcher fetcher = SQLRowValuesListFetcher.create(privateGraph);
fetcher.setSelTransf(new ITransformer<SQLSelect, SQLSelect>() {
@Override
public SQLSelect transformChecked(SQLSelect input) {
return input.andWhere(new Where(privateGraph.getTable().getKey(), ids));
}
});
final Set<SQLRow> rowsFetched = new HashSet<SQLRow>();
for (final SQLRowValues newVals : fetcher.fetch()) {
valsMap.put(newVals.getID(), newVals);
rowsFetched.add(newVals.asRow());
privates.collect(newVals);
}
if (!rowsFetched.equals(this.getRows()))
throw new IllegalStateException("Some rows are missing : " + rowsFetched + "\n" + this.getRows());
} else {
privates = null;
}
 
final Map<SQLRow, SQLRowValues> res = new HashMap<SQLRow, SQLRowValues>();
for (final SQLRow r : this.getRows()) {
SQLRowValues vals = valsMap.get(r.getID());
// when there's no private to fetch
if (vals == null) {
assert privates == null;
vals = r.createUpdateRow();
valsMap.put(r.getID(), vals);
}
hasBeen.put(r, vals);
res.put(r, vals);
}
expand(getElem().getTable(), valsMap, hasBeen, toCut, false);
if (privates != null)
privates.expand();
return Tuple2.create(res, toCut);
}
 
// NOTE using a collection of vals changed the time it took to archive a site (736 01) from 225s
// to 5s.
/**
138,13 → 179,22
* @param valsMap the values to expand, eg {3=>LOCAL(3)->BAT(4), 12=>LOCAL(12)->BAT(4)}.
* @param hasBeen the rows alredy expanded, eg {BAT[4], LOCAL[3], LOCAL[12]}.
* @param toCut the links to cut, eg {|BAT.ID_PRECEDENT|=> [BAT[2]]}.
* @param ignorePrivateParentRF <code>true</code> if
* {@link SQLElement#getPrivateParentReferentFields() private links} should be ignored.
* @throws SQLException if a link is {@link ReferenceAction#RESTRICT}.
*/
private final void expand(final SQLTable t, final Map<Integer, SQLRowValues> valsMap, final Set<SQLRow> hasBeen, final CollectionMap<SQLField, SQLRow> toCut) throws SQLException {
private final void expand(final SQLTable t, final Map<Integer, SQLRowValues> valsMap, final Map<SQLRow, SQLRowValues> hasBeen, final SetMap<SQLField, SQLRow> toCut,
final boolean ignorePrivateParentRF) throws SQLException {
if (valsMap.size() == 0)
return;
 
final Privates privates = new Privates(hasBeen, toCut);
final Set<SQLField> privateParentRF = ignorePrivateParentRF ? this.getElem().getElement(t).getPrivateParentReferentFields() : null;
for (final Link link : t.getDBSystemRoot().getGraph().getReferentLinks(t)) {
if (ignorePrivateParentRF && privateParentRF.contains(link.getLabel())) {
// if we did fetch the referents rows, they would be contained in hasBeen
continue;
}
// eg "ID_LOCAL"
final String ffName = link.getLabel().getName();
final SQLElement refElem = this.elem.getElementLenient(link.getSource());
154,7 → 204,16
throw new IllegalStateException("Null action for " + refElem + " " + ffName);
}
final Map<Integer, SQLRowValues> next = new HashMap<Integer, SQLRowValues>();
final SQLRowValuesListFetcher fetcher = new SQLRowValuesListFetcher(new SQLRowValues(link.getSource()).put(ffName, null));
final SQLRowValues graphToFetch;
if (action == ReferenceAction.CASCADE) {
// otherwise we would need to find and expand the parent rows of referents
if (refElem.getPrivateParentReferentFields().size() > 0)
throw new UnsupportedOperationException("Cannot cascade to private element " + refElem + " from " + link);
graphToFetch = refElem.getPrivateGraph(EnumSet.of(VirtualFields.FOREIGN_KEYS));
} else {
graphToFetch = new SQLRowValues(link.getSource()).put(ffName, null);
}
final SQLRowValuesListFetcher fetcher = SQLRowValuesListFetcher.create(graphToFetch);
fetcher.setSelTransf(new ITransformer<SQLSelect, SQLSelect>() {
@Override
public SQLSelect transformChecked(SQLSelect input) {
164,94 → 223,108
});
for (final SQLRowValues newVals : fetcher.fetch()) {
final SQLRow r = newVals.asRow();
final boolean already = hasBeen.containsKey(r);
switch (action) {
case RESTRICT:
throw new SQLException(createRestrictDesc(refElem, newVals, link));
case CASCADE:
if (!hasBeen.contains(r)) {
if (!already) {
// walk before linking to existing graph
privates.collect(newVals);
// link with existing graph, eg RECEPTEUR(235)->LOCAL(3)
newVals.put(ffName, valsMap.get(newVals.getInt(ffName)));
hasBeen.add(r);
hasBeen.put(r, newVals);
next.put(newVals.getID(), newVals);
}
break;
case SET_EMPTY:
// if the row should be archived no need to cut any of its links
if (!hasBeen.contains(r))
toCut.put(link.getLabel(), r);
if (!already)
toCut.add(link.getLabel(), r);
break;
}
// if already expanded just link and do not add to next
if (already) {
hasBeen.get(r).put(ffName, valsMap.get(newVals.getInt(ffName)));
}
}
 
expand(fetcher.getGraph().getTable(), next, hasBeen, toCut);
expand(fetcher.getGraph().getTable(), next, hasBeen, toCut, false);
}
// if the row should be archived no need to cut any of its links
final Iterator<Entry<SQLField, Collection<SQLRow>>> iter = toCut.entrySet().iterator();
privates.expand();
// if the row has been added to the graph (by another link) no need to cut any of its links
final Iterator<Entry<SQLField, Set<SQLRow>>> iter = toCut.entrySet().iterator();
while (iter.hasNext()) {
final Entry<SQLField, Collection<SQLRow>> e = iter.next();
e.getValue().removeAll(hasBeen);
final Entry<SQLField, Set<SQLRow>> e = iter.next();
final String fieldName = e.getKey().getName();
final Iterator<SQLRow> iter2 = e.getValue().iterator();
while (iter2.hasNext()) {
final SQLRow rowToCut = iter2.next();
final SQLRowValues inGraphRowToCut = hasBeen.get(rowToCut);
if (inGraphRowToCut != null) {
// remove from toCut
iter2.remove();
// add link
final SQLRowValues dest = hasBeen.get(rowToCut.getForeignRow(fieldName, SQLRowMode.NO_CHECK));
if (dest == null)
throw new IllegalStateException("destination of link to cut " + fieldName + " not found for " + rowToCut);
inGraphRowToCut.put(fieldName, dest);
}
}
if (e.getValue().isEmpty())
iter.remove();
}
}
 
// ***
private final class Privates {
private final Map<SQLRow, SQLRowValues> hasBeen;
private final SetMap<SQLField, SQLRow> toCut;
private final Map<SQLTable, Map<Integer, SQLRowValues>> privateRows;
 
/**
* Whether these trees contains a row with the same ID.
*
* @param r the row to search.
* @return <code>true</code> if there's a rowValues with the same ID as <code>r</code>.
* @throws SQLException if the trees could not be fetched.
*/
public final boolean contains(SQLRow r) throws SQLException {
for (final SQLRowValues g : getTrees()) {
for (final SQLRowValues desc : g.getGraph().getItems()) {
if (r.equals(desc.asRow()))
return true;
public Privates(final Map<SQLRow, SQLRowValues> hasBeen, final SetMap<SQLField, SQLRow> toCut) {
this.hasBeen = hasBeen;
this.toCut = toCut;
this.privateRows = new HashMap<SQLTable, Map<Integer, SQLRowValues>>();
}
 
private void collect(final SQLRowValues mainRow) {
for (final SQLRowValues privateVals : mainRow.getGraph().getItems()) {
if (privateVals != mainRow) {
// since newVals isn't in, its privates can't
assert !this.hasBeen.containsKey(privateVals.asRow());
Map<Integer, SQLRowValues> map = this.privateRows.get(privateVals.getTable());
if (map == null) {
map = new HashMap<Integer, SQLRowValues>();
this.privateRows.put(privateVals.getTable(), map);
}
return false;
map.put(privateVals.getID(), privateVals);
}
}
}
 
/**
* Put all the rows of the trees (except the roots) in a map by table.
*
* @return the descendants by table.
* @throws SQLException if the trees could not be fetched.
*/
public final CollectionMap<SQLTable, SQLRowAccessor> getDescendantsByTable() throws SQLException {
final CollectionMap<SQLTable, SQLRowAccessor> res = new CollectionMap<SQLTable, SQLRowAccessor>();
for (final SQLRowValues graph : this.getTrees()) {
graph.getGraph().walk(graph, null, new ITransformer<State<Object>, Object>() {
@Override
public Object transformChecked(State<Object> input) {
if (graph != input.getCurrent())
res.put(input.getCurrent().getTable(), input.getCurrent());
return null;
private void expand() throws SQLException {
for (final Entry<SQLTable, Map<Integer, SQLRowValues>> e : this.privateRows.entrySet()) {
TreesOfSQLRows.this.expand(e.getKey(), e.getValue(), this.hasBeen, this.toCut, true);
}
}, RecursionType.BREADTH_FIRST, Direction.REFERENT);
}
return res;
}
 
// ***
 
/**
* Returns the descendants as a list that is ordered "leaves-first", ie the last item is a root.
* Put all the rows of the trees (except the roots) in a map by table.
*
* @return a flat list of the descendants.
* @return the descendants by table.
* @throws SQLException if the trees could not be fetched.
*
*/
public final List<SQLRowAccessor> getFlatDescendants() throws SQLException {
final List<SQLRowAccessor> res = new ArrayList<SQLRowAccessor>();
for (final SQLRowValues graph : this.getTrees()) {
graph.getGraph().walk(graph, null, new ITransformer<State<Object>, Object>() {
@Override
public Object transformChecked(State<Object> input) {
res.add(input.getCurrent());
return null;
public final Map<SQLTable, List<SQLRowAccessor>> getDescendantsByTable() throws SQLException {
final ListMap<SQLTable, SQLRowAccessor> res = new ListMap<SQLTable, SQLRowAccessor>();
final Set<SQLRow> roots = this.getRows();
for (final SQLRowValuesCluster c : this.getClusters()) {
for (final SQLRowValues v : c.getItems()) {
if (!roots.contains(v.asRow()))
res.add(v.getTable(), v);
}
}, RecursionType.DEPTH_FIRST, Direction.REFERENT);
}
return res;
}
264,7 → 337,7
* @return the rows by referent field.
* @throws SQLException if the trees could not be fetched.
*/
public final CollectionMap<SQLField, SQLRow> getExternReferences() throws SQLException {
public final Map<SQLField, Set<SQLRow>> getExternReferences() throws SQLException {
// force compute
getTrees();
return this.externReferences;
277,7 → 350,7
return o1.getSQLName().toString().compareTo(o2.getSQLName().toString());
}
});
for (final Map.Entry<SQLField, Collection<SQLRow>> e : this.getExternReferences().entrySet()) {
for (final Map.Entry<SQLField, Set<SQLRow>> e : this.getExternReferences().entrySet()) {
res.put(e.getKey(), e.getValue().size());
}
return res;
/trunk/OpenConcerto/src/org/openconcerto/sql/element/DefaultElementSQLObject.java
56,7 → 56,6
private JButton createBtn;
private JButton supprBtn;
private JSeparator separator;
private boolean isDecorated = true;
private boolean isSeparatorVisible = true;
private JPanel editP;
private JPanel createP;
79,16 → 78,22
}
 
public void setDecorated(boolean decorated) {
this.isDecorated = decorated;
if (this.expandBtn != null)
this.expandBtn.setVisible(decorated);
if (this.supprBtn != null)
this.supprBtn.setVisible(decorated);
if (this.createBtn != null)
this.createBtn.setVisible(decorated);
}
 
@Override
protected void uiChanged() {
super.uiChanged();
updateBtns();
}
 
protected void updateBtns() {
this.createBtn.setVisible(this.isCreatedUIVisible());
this.supprBtn.setVisible(this.isCreatedUIVisible() && this.deleteAllowed() && !Boolean.FALSE.equals(this.isExpanded()));
}
 
@Override
protected void uiInit() {
final boolean isPlastic = UIManager.getLookAndFeel().getClass().getName().startsWith("com.jgoodies.plaf.plastic");
 
137,11 → 142,11
if (this.editP != null)
this.editP.setVisible(false);
this.getCreatePanel().setVisible(true);
this.createBtn.requestFocusInWindow();
// don't call setCurrentID() otherwise, when updating we won't know
// what observation to archive
this.revalidate();
this.repaint();
this.expanded = false;
}
 
private final JPanel getCreatePanel() {
157,10 → 162,10
 
@Override
protected final void setEditPanel() {
this.supprBtn.setVisible(!this.required && this.isDecorated);
if (this.createP != null)
this.createP.setVisible(false);
this.getEditPanel().setVisible(true);
this.getEditPanel().requestFocusInWindow();
// toujours afficher un composant vierge à la création
// do it after add() as it calls SQLRowView.activate() which refresh comp from the db
// possibly resurecting a deleted row
218,8 → 223,14
this.expandBtn.setEnabled(this.getCurrentID() != SQLRow.NONEXISTANT_ID && this.getValidState().isValid());
}
 
private final boolean isExpanded() {
return this.expanded;
/**
* Whether the edit panel is expanded.
*
* @return <code>true</code> if the edit panel is expanded, <code>false</code> if not and
* <code>null</code> if not {@link #isCreated()}.
*/
private final Boolean isExpanded() {
return this.isCreated() ? this.expanded : null;
}
 
private void expand(boolean b) {
227,11 → 238,10
throw new IllegalStateException("cannot expand if not created");
}
 
this.expanded = b;
this.updateBtns();
this.getSQLChild().setVisible(b);
if (!this.required)
this.supprBtn.setVisible(b);
this.validate();
this.expanded = b;
}
 
private void toggleExpand() {
/trunk/OpenConcerto/src/org/openconcerto/sql/element/ArchivedGraph.java
New file
0,0 → 1,278
/*
* 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 org.openconcerto.sql.model.SQLField;
import org.openconcerto.sql.model.SQLRow;
import org.openconcerto.sql.model.SQLRowAccessor;
import org.openconcerto.sql.model.SQLRowValues;
import org.openconcerto.sql.model.SQLRowValuesListFetcher;
import org.openconcerto.sql.model.SQLSelect;
import org.openconcerto.sql.model.SQLSelect.ArchiveMode;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.SQLTable.VirtualFields;
import org.openconcerto.sql.model.Where;
import org.openconcerto.sql.model.graph.Link;
import org.openconcerto.utils.ListMap;
import org.openconcerto.utils.SetMap;
import org.openconcerto.utils.cc.ITransformer;
 
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
 
/**
* Allow to find which rows must be unarchived to maintain coherency.
*
* @author Sylvain
*/
final class ArchivedGraph {
 
static private final EnumSet<VirtualFields> ARCHIVE_AND_FOREIGNS = EnumSet.of(VirtualFields.FOREIGN_KEYS, VirtualFields.ARCHIVE);
 
private final SQLElementDirectory dir;
private final SQLRowValues graph;
// indexed nodes of graph
private final Map<SQLRow, SQLRowValues> graphRows;
// rows that haven't been processed
private final Set<SQLRow> toExpand;
 
/**
* Create a new instance.
*
* @param dir the directory.
* @param graph the rows (without privates) to unarchive. This object will be modified.
*/
ArchivedGraph(final SQLElementDirectory dir, final SQLRowValues graph) {
if (dir == null)
throw new NullPointerException("Null SQLElementDirectory");
this.dir = dir;
this.graph = graph;
this.graphRows = new HashMap<SQLRow, SQLRowValues>();
for (final SQLRowValues v : this.graph.getGraph().getItems()) {
final SQLRowValues prev = this.graphRows.put(v.asRow(), v);
if (prev != null)
throw new IllegalStateException("Duplicated row : " + v.asRow());
}
assert this.graphRows.size() == this.graph.getGraph().size();
this.toExpand = new HashSet<SQLRow>(this.graphRows.keySet());
}
 
private final SQLElement getElement(final SQLTable t) {
return this.dir.getElement(t);
}
 
private void expandPrivates() {
final SetMap<SQLTable, Number> idsToExpandPrivate = new SetMap<SQLTable, Number>();
for (final SQLRow toExpPrivate : this.toExpand) {
idsToExpandPrivate.add(toExpPrivate.getTable(), toExpPrivate.getIDNumber());
}
for (final Entry<SQLTable, Set<Number>> e : idsToExpandPrivate.entrySet()) {
final SQLElement elem = getElement(e.getKey());
final Set<Number> ids = e.getValue();
final SQLRowValues privateGraph = elem.getPrivateGraph(ARCHIVE_AND_FOREIGNS);
if (privateGraph.getGraph().size() > 1) {
final SQLRowValuesListFetcher fetcher = SQLRowValuesListFetcher.create(privateGraph, false);
setWhereAndArchivePolicy(fetcher, ids, ArchiveMode.BOTH);
final List<SQLRowValues> fetchedRows = fetcher.fetch();
assert fetchedRows.size() == ids.size();
for (final SQLRowValues valsFetched : fetchedRows) {
// attach to existing graph
// need to copy since we modify the graph when loading values
for (final SQLRowValues v : new ArrayList<SQLRowValues>(valsFetched.getGraph().getItems())) {
final SQLRow row = v.asRow();
if (v == valsFetched) {
final SQLRowValues toExpandVals = this.graphRows.get(row);
toExpandVals.load(valsFetched, null);
assert valsFetched.getFields().isEmpty();
for (final Entry<SQLField, ? extends Collection<SQLRowValues>> refEntry : new ListMap<SQLField, SQLRowValues>(valsFetched.getReferentsMap()).entrySet()) {
for (final SQLRowValues ref : refEntry.getValue()) {
ref.put(e.getKey().getName(), toExpandVals);
}
}
assert valsFetched.getGraph().size() == 1;
} else {
this.toExpand.add(row);
this.graphRows.put(row, v);
}
}
}
}
}
}
 
final SQLRowValues expand() {
// expand once the privates (in the rest of this method they are fetched alongside the
// main row)
expandPrivates();
 
while (!this.toExpand.isEmpty()) {
// find required archived rows
final SetMap<SQLTable, Number> toFetch = new SetMap<SQLTable, Number>();
final SetMap<SQLTable, Number> privateToFetch = new SetMap<SQLTable, Number>();
final Map<SQLTable, Set<Link>> foreignFields = new HashMap<SQLTable, Set<Link>>();
final SetMap<SQLRow, String> nonEmptyFieldsPointingToPrivates = new SetMap<SQLRow, String>();
for (final SQLRow rowToExpand : this.toExpand) {
final SQLTable t = rowToExpand.getTable();
Set<Link> ffs = foreignFields.get(t);
if (ffs == null) {
ffs = t.getDBSystemRoot().getGraph().getForeignLinks(t);
foreignFields.put(t, ffs);
}
for (final Link ff : ffs) {
final String fieldName = ff.getLabel().getName();
if (!rowToExpand.isForeignEmpty(fieldName)) {
final SQLRow foreignRow = new SQLRow(ff.getTarget(), rowToExpand.getInt(fieldName));
final SQLRowValues existingRow = this.graphRows.get(foreignRow);
if (existingRow != null) {
this.graphRows.get(rowToExpand).put(fieldName, existingRow);
} else {
final SQLElement elem = getElement(foreignRow.getTable());
final SetMap<SQLTable, Number> map;
if (elem.getPrivateParentReferentFields().size() > 0) {
// if foreignRow is part of private graph, fetch it later from
// the main row
nonEmptyFieldsPointingToPrivates.add(rowToExpand, fieldName);
map = privateToFetch;
} else {
map = toFetch;
}
map.add(foreignRow.getTable(), foreignRow.getIDNumber());
}
}
}
}
 
final Map<SQLRow, SQLRowValues> archivedForeignRows = fetch(toFetch);
 
// attach to existing graph
final Map<SQLRow, SQLRowValues> added = new HashMap<SQLRow, SQLRowValues>();
for (final SQLRow rowToExpand : this.toExpand) {
final SQLTable t = rowToExpand.getTable();
final Set<Link> ffs = foreignFields.get(t);
assert ffs != null;
for (final Link ff : ffs) {
final String fieldName = ff.getLabel().getName();
if (!rowToExpand.isForeignEmpty(fieldName)) {
final SQLRow foreignRow = new SQLRow(ff.getTarget(), rowToExpand.getInt(fieldName));
final SQLRowValues valsFetched = archivedForeignRows.get(foreignRow);
// null meaning excluded because it wasn't archived or points to a
// private
if (valsFetched != null && valsFetched.isArchived()) {
attach(rowToExpand, fieldName, valsFetched, added);
// rows were fetched from a different link
nonEmptyFieldsPointingToPrivates.remove(rowToExpand, fieldName);
privateToFetch.remove(foreignRow.getTable(), foreignRow.getIDNumber());
}
}
}
}
 
// only referenced archived rows
final Map<SQLRow, SQLRowValues> privateFetched = fetch(privateToFetch);
toFetch.clear();
for (final SQLRow r : privateFetched.keySet()) {
final SQLRowAccessor privateRoot = getElement(r.getTable()).getPrivateRoot(r, ArchiveMode.BOTH);
toFetch.add(privateRoot.getTable(), privateRoot.getIDNumber());
}
// then fetch private graph (even if the private row referenced is archived its main
// row might not be)
final Map<SQLRow, SQLRowValues> mainRowFetched = fetch(toFetch, ArchiveMode.BOTH);
// attach to existing graph
for (final Entry<SQLRow, Set<String>> e : nonEmptyFieldsPointingToPrivates.entrySet()) {
final SQLRow rowToExpand = e.getKey();
final SQLTable t = rowToExpand.getTable();
for (final String fieldName : e.getValue()) {
assert !rowToExpand.isForeignEmpty(fieldName);
final SQLRow foreignRow = new SQLRow(t.getForeignTable(fieldName), rowToExpand.getInt(fieldName));
final SQLRowValues valsFetched = mainRowFetched.get(foreignRow);
if (valsFetched != null) {
// since we kept only archived ones in privateFetched
assert valsFetched.isArchived();
attach(rowToExpand, fieldName, valsFetched, added);
}
}
}
 
this.toExpand.clear();
this.toExpand.addAll(added.keySet());
}
return this.graph;
}
 
// add a link through fieldName and if this joins 2 graphs, index the new rows.
private void attach(final SQLRow rowToExpand, final String fieldName, final SQLRowValues valsFetched, final Map<SQLRow, SQLRowValues> added) {
final boolean alreadyLinked = this.graph.getGraph() == valsFetched.getGraph();
this.graphRows.get(rowToExpand).put(fieldName, valsFetched);
if (!alreadyLinked) {
// put all values of valsFetched
for (final SQLRowValues v : valsFetched.getGraph().getItems()) {
final SQLRow row = v.asRow();
added.put(row, v);
this.graphRows.put(row, v);
}
}
assert this.graphRows.size() == this.graph.getGraph().size();
}
 
private void setWhereAndArchivePolicy(final SQLRowValuesListFetcher fetcher, final Set<Number> ids, final ArchiveMode archiveMode) {
for (final SQLRowValuesListFetcher f : fetcher.getFetchers(true).allValues()) {
f.setSelTransf(new ITransformer<SQLSelect, SQLSelect>() {
@Override
public SQLSelect transformChecked(SQLSelect input) {
if (f == fetcher) {
input.andWhere(new Where(fetcher.getGraph().getTable().getKey(), ids));
}
input.setArchivedPolicy(archiveMode);
return input;
}
});
}
}
 
private Map<SQLRow, SQLRowValues> fetch(final SetMap<SQLTable, Number> toFetch) {
return this.fetch(toFetch, ArchiveMode.ARCHIVED);
}
 
// fetch the passed rows (and their privates if the table is a main one)
private Map<SQLRow, SQLRowValues> fetch(final SetMap<SQLTable, Number> toFetch, final ArchiveMode archiveMode) {
final Map<SQLRow, SQLRowValues> res = new HashMap<SQLRow, SQLRowValues>();
for (final Entry<SQLTable, Set<Number>> e : toFetch.entrySet()) {
final Set<Number> ids = e.getValue();
final SQLTable table = e.getKey();
final SQLElement elem = getElement(table);
final SQLRowValuesListFetcher fetcher;
// don't fetch partial data
if (elem.getPrivateParentReferentFields().isEmpty())
fetcher = SQLRowValuesListFetcher.create(elem.getPrivateGraph(ARCHIVE_AND_FOREIGNS));
else
fetcher = new SQLRowValuesListFetcher(new SQLRowValues(table).putNulls(table.getFieldsNames(ARCHIVE_AND_FOREIGNS)));
setWhereAndArchivePolicy(fetcher, ids, archiveMode);
for (final SQLRowValues fetchedVals : fetcher.fetch()) {
for (final SQLRowValues v : fetchedVals.getGraph().getItems()) {
final SQLRow r = v.asRow();
res.put(r, v);
assert !this.graphRows.containsKey(r) : "already in graph : " + r;
}
}
}
return res;
}
}
/trunk/OpenConcerto/src/org/openconcerto/sql/element/UpdateScript.java
13,12 → 13,12
package org.openconcerto.sql.element;
 
import org.openconcerto.sql.model.SQLRowAccessor;
import org.openconcerto.sql.model.SQLRowValues;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.utils.CollectionMap;
import org.openconcerto.utils.SetMap;
 
import java.sql.SQLException;
import java.util.HashSet;
 
/**
* Apply updates to a row (including archiving obsolete privates).
27,11 → 27,11
*/
public final class UpdateScript {
private final SQLRowValues updateRow;
private final CollectionMap<SQLElement, SQLRowValues> toArchive;
private final SetMap<SQLElement, SQLRowAccessor> toArchive;
 
UpdateScript(final SQLTable t) {
this.updateRow = new SQLRowValues(t);
this.toArchive = new CollectionMap<SQLElement, SQLRowValues>(HashSet.class);
this.toArchive = new SetMap<SQLElement, SQLRowAccessor>();
}
 
final SQLRowValues getUpdateRow() {
38,8 → 38,8
return this.updateRow;
}
 
final void addToArchive(SQLElement elem, SQLRowValues r) {
this.toArchive.put(elem, r);
final void addToArchive(SQLElement elem, SQLRowAccessor r) {
this.toArchive.add(elem, r);
}
 
final void put(String field, UpdateScript s) {
55,7 → 55,7
public final void exec() throws SQLException {
this.getUpdateRow().commit();
for (final SQLElement elem : this.toArchive.keySet()) {
for (final SQLRowValues v : this.toArchive.getNonNull(elem))
for (final SQLRowAccessor v : this.toArchive.getNonNull(elem))
elem.archive(v.getID());
}
}
/trunk/OpenConcerto/src/org/openconcerto/sql/element/SQLElement.java
24,12 → 24,20
import org.openconcerto.sql.model.SQLRowAccessor;
import org.openconcerto.sql.model.SQLRowMode;
import org.openconcerto.sql.model.SQLRowValues;
import org.openconcerto.sql.model.SQLRowValues.ForeignCopyMode;
import org.openconcerto.sql.model.SQLRowValuesCluster;
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.SQLSelect;
import org.openconcerto.sql.model.SQLSelect.ArchiveMode;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.SQLTable.VirtualFields;
import org.openconcerto.sql.model.Where;
import org.openconcerto.sql.model.graph.DatabaseGraph;
import org.openconcerto.sql.model.graph.Link;
import org.openconcerto.sql.model.graph.Link.Direction;
import org.openconcerto.sql.request.ComboSQLRequest;
import org.openconcerto.sql.request.ListSQLRequest;
import org.openconcerto.sql.request.SQLCache;
42,11 → 50,17
import org.openconcerto.sql.view.list.SQLTableModelColumn;
import org.openconcerto.sql.view.list.SQLTableModelColumnPath;
import org.openconcerto.sql.view.list.SQLTableModelSourceOnline;
import org.openconcerto.ui.group.Group;
import org.openconcerto.utils.CollectionMap;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.ExceptionHandler;
import org.openconcerto.utils.ExceptionUtils;
import org.openconcerto.utils.LinkedListMap;
import org.openconcerto.utils.ListMap;
import org.openconcerto.utils.NumberUtils;
import org.openconcerto.utils.RecursionType;
import org.openconcerto.utils.SetMap;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.utils.cache.CacheResult;
import org.openconcerto.utils.cc.IClosure;
66,14 → 80,17
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 javax.swing.JComponent;
82,10 → 99,6
 
import net.jcip.annotations.GuardedBy;
 
import org.apache.commons.collections.MapIterator;
import org.apache.commons.collections.MultiMap;
import org.apache.commons.collections.iterators.EntrySetMapIterator;
 
/**
* Décrit comment manipuler un élément de la BD (pas forcément une seule table, voir
* privateForeignField).
147,7 → 160,7
private ListSQLRequest list;
private SQLTableModelSourceOnline tableSrc;
private final ListChangeRecorder<IListeAction> rowActions;
private final CollectionMap<String, ITransformer<Tuple2<SQLElement, String>, SQLComponent>> components;
private final LinkedListMap<String, ITransformer<Tuple2<SQLElement, String>, SQLComponent>> components;
// foreign fields
private Set<String> normalFF;
private String parentFF;
165,6 → 178,7
private final List<SQLTableModelColumn> additionalListCols;
@GuardedBy("this")
private List<String> mdPath;
private Group defaultGroup;
 
@Deprecated
public SQLElement(String singular, String plural, SQLTable primaryTable) {
194,7 → 208,7
this.actions = new HashMap<String, ReferenceAction>();
this.resetRelationships();
 
this.components = new CollectionMap<String, ITransformer<Tuple2<SQLElement, String>, SQLComponent>>(new LinkedList<ITransformer<Tuple2<SQLElement, String>, SQLComponent>>());
this.components = new LinkedListMap<String, ITransformer<Tuple2<SQLElement, String>, SQLComponent>>();
 
this.modelCache = null;
 
214,6 → 228,14
return getClass().getName() + "-" + getTable().getName();
}
 
public Group getDefaultGroup() {
return defaultGroup;
}
 
public void setDefaultGroup(Group defaultGroup) {
this.defaultGroup = defaultGroup;
}
 
/**
* Must be called if foreign/referent keys are added or removed.
*/
267,6 → 289,8
this.parentFF = parents.size() == 0 ? null : (String) parents.iterator().next();
if (privates.size() > 0)
throw new IllegalStateException("for " + this + " these private foreign fields are not valid :" + privates);
assert assertPrivateDefaultValues();
 
this.sharedFF = tmpSharedFF;
 
// MAYBE move required fields to SQLElement and use RESTRICT
283,6 → 307,17
this.ffInited();
}
 
// since by definition private cannot be shared, the default value must be empty
private final boolean assertPrivateDefaultValues() {
for (final Entry<String, SQLElement> e : this.privateFF.entrySet()) {
final String fieldName = e.getKey();
final Number privateDefault = (Number) getTable().getField(fieldName).getParsedDefaultValue().getValue();
final Number foreignUndef = e.getValue().getTable().getUndefinedIDNumber();
assert NumberUtils.areNumericallyEqual(privateDefault, foreignUndef) : fieldName + " not empty : " + privateDefault;
}
return true;
}
 
protected void ffInited() {
// MAYBE use DELETE_RULE of Link
}
504,56 → 539,6
return this.modelCache;
}
 
public void unarchiveNonRec(int id) throws SQLException {
this.unarchiveNonRec(this.getTable().getRow(id));
}
 
private void unarchiveNonRec(SQLRow row) throws SQLException {
checkUndefined(row);
if (!row.isArchived())
return;
 
final Set<SQLRow> connectedRows = this.getArchivedConnectedRows(Collections.singleton(row));
for (final SQLRow desc : connectedRows) {
getElement(desc.getTable()).unarchiveSingle(desc);
}
for (final SQLRow desc : connectedRows) {
DeletionMode.UnArchiveMode.fireChange(desc);
}
}
 
// *** getConnected*
 
private Set<SQLRow> getArchivedConnectedRows(Collection<SQLRow> rows) throws SQLException {
final Set<SQLRow> res = new HashSet<SQLRow>();
for (final SQLRow row : rows) {
this.getElement(row.getTable()).getArchivedConnectedRows(row, res);
}
return res;
}
 
private void getArchivedConnectedRows(SQLRow row, Set<SQLRow> rows) throws SQLException {
check(row);
// si on était déjà dedans, ne pas continuer
if (!rows.add(row))
return;
 
// we want ARCHIVED existant and defined rows (since we never touch undefined ones)
final SQLRowMode mode = new SQLRowMode(ArchiveMode.ARCHIVED, true, true);
final Set<SQLRow> foreigns = new HashSet<SQLRow>(this.getNormalForeigns(row, mode).values());
final SQLRow parent = this.getForeignParent(row, mode);
if (parent != null) {
foreigns.add(parent);
}
// private ff are handled by DeletionMode
// shared ff are never touched by DeletionMode
 
// recurse
for (final SQLRow foreign : foreigns) {
this.getElement(foreign.getTable()).getArchivedConnectedRows(foreign, rows);
}
}
 
// *** update
 
/**
580,76 → 565,70
} else {
final Object fromPrivate = from.getObject(field);
final Object toPrivate = to.getObject(field);
if (!fromPrivate.getClass().equals(toPrivate.getClass()))
throw new IllegalArgumentException("asymmetric tree " + fromPrivate + " != " + toPrivate);
final boolean isPrivate = this.getPrivateForeignFields().contains(field);
if (fromPrivate instanceof SQLRowValues) {
final SQLRowValues fromPR = (SQLRowValues) fromPrivate;
final SQLElement privateElem = this.getPrivateElement(field);
if (privateElem != null) {
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;
if (isPrivate) {
if (from.isForeignEmpty(field) && to.isForeignEmpty(field)) {
// nothing to do, don't add to v
} else if (from.isForeignEmpty(field)) {
// insert, eg CPI.ID_OBS=1 -> CPI.ID_OBS={DES="rouillé"}
// clear referents otherwise we will merge the updateRow with the to
// graph (toPR being a private is pointed to by its owner, which itself
// points to others, but we just want the private)
res.getUpdateRow().put(field, toPR.deepCopy().clearReferents());
} else if (to.isForeignEmpty(field)) {
} else if (toIsEmpty) {
// archive
res.addToArchive(this.getForeignElement(field), fromPR);
res.addToArchive(privateElem, from.getForeign(field));
} else {
// neither is empty
if (fromPR.getID() != toPR.getID())
throw new IllegalArgumentException("private have changed: " + fromPR + " != " + toPR);
res.put(field, this.getForeignElement(field).update(fromPR, toPR));
if (!CompareUtils.equals(from.getForeignID(field), to.getForeignID(field)))
throw new IllegalArgumentException("private have changed for " + field + " : " + fromPrivate + " != " + toPrivate);
if (toPrivate instanceof SQLRowValues) {
final SQLRowValues fromPR = (SQLRowValues) fromPrivate;
final SQLRowValues toPR = (SQLRowValues) toPrivate;
// must have same ID
res.put(field, privateElem.update(fromPR, toPR));
}
} else {
res.getUpdateRow().put(field, toPR.getID());
}
} else if (to.isDefault(field)) {
res.getUpdateRow().putDefault(field);
} else {
final Number fromP_ID = (Number) fromPrivate;
final Number toP_ID = (Number) toPrivate;
if (isPrivate) {
// avoid Integer(3) != Long(3)
if (fromP_ID.longValue() != toP_ID.longValue())
throw new IllegalArgumentException("cannot change private ID");
// if they're the same, nothing to do
} else {
res.getUpdateRow().put(field, toP_ID);
res.getUpdateRow().put(field, to.getForeignIDNumber(field));
}
}
}
}
 
return res;
}
 
public 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 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.
 
// nos descendants
final List<SQLRow> descsAndMe = this.getTree(row, true);
final Set<SQLRow> connectedRows = this.getArchivedConnectedRows(descsAndMe);
final SQLRowValues descsAndMe = desc ? this.getTree(row, true) : row.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 {
for (final SQLRow desc : connectedRows) {
getElement(desc.getTable()).unarchiveSingle(desc);
}
for (final SQLRow desc : connectedRows) {
DeletionMode.UnArchiveMode.fireChange(desc);
}
 
// reference
// nothing to do : nobody points to an archived row
 
setArchive(Collections.singletonList(connectedRows.getGraph()), false);
return null;
}
});
659,6 → 638,11
this.archive(this.getTable().getRow(id));
}
 
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);
}
695,17 → 679,13
// 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 MultiMap externReferences = trees.getExternReferences();
final Map<SQLField, Set<SQLRow>> externReferences = trees.getExternReferences();
// avoid toString() which might make requests to display rows (eg archived)
if (Log.get().isLoggable(Level.FINEST))
Log.get().finest("will cut : " + externReferences);
final MapIterator refIter = new EntrySetMapIterator(externReferences);
while (refIter.hasNext()) {
final SQLField refKey = (SQLField) refIter.next();
final Collection<?> refList = (Collection<?>) refIter.getValue();
final Iterator<?> listIter = refList.iterator();
while (listIter.hasNext()) {
final SQLRow ref = (SQLRow) listIter.next();
for (final Entry<SQLField, Set<SQLRow>> e : externReferences.entrySet()) {
final SQLField refKey = e.getKey();
for (final SQLRow ref : e.getValue()) {
ref.createEmptyUpdateRow().putEmptyLink(refKey.getName()).update();
}
}
713,41 → 693,174
}
 
// on archive tous nos descendants
for (final SQLRowAccessor desc : trees.getFlatDescendants()) {
getElement(desc.getTable()).archiveSingle(desc);
// ne pas faire les fire après sinon qd on efface plusieurs éléments de la même
// table :
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();
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
DeletionMode.ArchiveMode.fireChange(desc);
setArchive(updateVals, archive).setID(desc.getIDNumber());
// don't check validity since table events might have not already be
// fired
assert updateVals.getGraph().size() == 1 : "Archiving a graph : " + updateVals.printGraph();
updateVals.getGraph().store(StoreMode.COMMIT, false);
// remove from graph
desc.clear();
desc.clearReferents();
assert desc.getGraph().size() == 1 : "Next loop won't progress : " + desc.printGraph();
archivedCount++;
iter.remove();
}
// foreign field
// nothing to do
 
return null;
}
});
}
 
private final void archiveSingle(SQLRowAccessor r) throws SQLException {
this.changeSingle(r, DeletionMode.ArchiveMode);
// 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();
}
 
private final void unarchiveSingle(SQLRowAccessor r) throws SQLException {
this.changeSingle(r, DeletionMode.UnArchiveMode);
return null;
}
}, new WalkOptions(Direction.REFERENT).setRecursionType(RecursionType.BREADTH_FIRST).setStartIncluded(false).setCycleAllowed(true));
final SQLRowValues cutLinks = cutLinksRef.get();
 
private final void changeSingle(SQLRowAccessor r, DeletionMode m) throws SQLException {
m.execute(this, r);
// 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.");
 
this.changeSingle(r, DeletionMode.DeleteMode);
}
 
public final SQLTable getTable() {
899,15 → 1012,36
}
 
/**
* The graph of this table and its private fields.
* The graph of this table and its privates.
*
* @return a rowValues of this element's table with rowValues for each private foreign field.
* @return an SQLRowValues of this element's table filled with
* {@link SQLRowValues#setAllToNull() <code>null</code>s} except for private foreign
* fields containing SQLRowValues.
*/
public final SQLRowValues getPrivateGraph() {
return this.getPrivateGraph(null);
}
 
/**
* The graph of this table and its privates.
*
* @param fields which fields should be included in the graph, <code>null</code> meaning all.
* @return an SQLRowValues of this element's table filled with <code>null</code>s according to
* the <code>fields</code> parameter except for private foreign fields containing
* SQLRowValues.
*/
public final SQLRowValues getPrivateGraph(final Set<VirtualFields> fields) {
final SQLRowValues res = new SQLRowValues(this.getTable());
if (fields == null) {
res.setAllToNull();
} else {
for (final VirtualFields vf : fields) {
for (final SQLField f : this.getTable().getFields(vf))
res.put(f.getName(), null);
}
}
for (final Entry<String, SQLElement> e : this.getPrivateFF().entrySet()) {
res.put(e.getKey(), e.getValue().getPrivateGraph());
res.put(e.getKey(), e.getValue().getPrivateGraph(fields));
}
return res;
}
1250,6 → 1384,23
* @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;
1262,7 → 1413,7
public SQLRow create() throws SQLException {
 
// eg SITE[128]
final SQLRowValues copy = createTransformedCopy(row, parent, c);
final SQLRowValues copy = createTransformedCopy(row, full, parent, c);
copies.put(row, copy);
 
forDescendantsDo(row, new ChildProcessor<SQLRow>() {
1270,18 → 1421,18
final SQLRowValues parentCopy = copies.get(parent);
if (parentCopy == null)
throw new IllegalStateException("null copy of " + parent);
final SQLRowValues descCopy = createTransformedCopy(desc, null, c);
final SQLRowValues descCopy = createTransformedCopy(desc, full, null, c);
descCopy.put(joint.getName(), parentCopy);
copies.put(desc, descCopy);
}
}, false, false, false);
}, full, false, false);
// ne pas descendre en deep
 
// reference
forDescendantsDo(row, new ChildProcessor<SQLRow>() {
public void process(SQLRow parent, SQLField joint, SQLRow desc) throws SQLException {
final CollectionMap<SQLField, SQLRow> normalReferents = getElement(desc.getTable()).getNonChildrenReferents(desc);
for (final Entry<SQLField, Collection<SQLRow>> e : normalReferents.entrySet()) {
final Map<SQLField, List<SQLRow>> normalReferents = getElement(desc.getTable()).getNonChildrenReferents(desc);
for (final Entry<SQLField, List<SQLRow>> e : normalReferents.entrySet()) {
// eg SOURCE.ID_CPI
final SQLField refField = e.getKey();
for (final SQLRow ref : e.getValue()) {
1295,7 → 1446,7
}
}
}
}, false);
}, 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,
1306,8 → 1457,8
});
}
 
private final SQLRowValues createTransformedCopy(SQLRow desc, SQLRow parent, final IClosure<SQLRowValues> c) throws SQLException {
final SQLRowValues copiedVals = getElement(desc.getTable()).createCopy(desc, parent);
private final SQLRowValues createTransformedCopy(SQLRow desc, final boolean full, SQLRow parent, final IClosure<SQLRowValues> c) throws SQLException {
final SQLRowValues copiedVals = getElement(desc.getTable()).createCopy(desc, full, parent);
assert copiedVals != null : "failed to copy " + desc;
if (c != null)
c.executeChecked(copiedVals);
1343,7 → 1494,11
* @return a copy ready to be inserted, or <code>null</code> if <code>row</code> cannot be
* copied.
*/
public SQLRowValues createCopy(SQLRow row, SQLRow parent) {
public SQLRowValues createCopy(SQLRowAccessor row, SQLRow parent) {
return createCopy(row, false, parent);
}
 
public SQLRowValues createCopy(SQLRowAccessor row, final boolean full, SQLRow parent) {
// do NOT copy the undefined
if (row == null || row.isUndefined())
return null;
1350,12 → 1505,13
this.check(row);
 
final SQLRowValues copy = new SQLRowValues(this.getTable());
this.loadAllSafe(copy, row);
this.loadAllSafe(copy, row.asRow());
 
for (final String privateName : this.getPrivateForeignFields()) {
final SQLElement privateElement = this.getPrivateElement(privateName);
if (!privateElement.dontDeepCopy() && !row.isForeignEmpty(privateName)) {
final SQLRowValues child = privateElement.createCopy(row.getInt(privateName));
final boolean deepCopy = full || !privateElement.dontDeepCopy();
if (deepCopy && !row.isForeignEmpty(privateName)) {
final SQLRowValues child = privateElement.createCopy(row.getForeign(privateName), full, null);
copy.put(privateName, child);
} else {
copy.putEmptyLink(privateName);
1398,13 → 1554,13
* @param row a SQLRow.
* @return the descendant rows by SQLTable.
*/
public final CollectionMap<SQLTable, SQLRow> getDescendants(SQLRow row) {
public final ListMap<SQLTable, SQLRow> getDescendants(SQLRow row) {
check(row);
final CollectionMap<SQLTable, SQLRow> mm = new CollectionMap<SQLTable, SQLRow>();
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.put(joint.getTable(), child);
mm.add(joint.getTable(), child);
}
}, true);
} catch (SQLException e) {
1415,30 → 1571,26
}
 
/**
* Returns the tree beneath the passed row. The list is ordered "leaves-first", ie the last item
* is the root.
* 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 a List of SQLRow.
* @return the asked tree.
*/
private List<SQLRow> getTree(SQLRow row, boolean archived) {
private SQLRowValues getTree(SQLRow row, boolean archived) {
check(row);
// nos descendants
final List<SQLRow> descsAndMe = new ArrayList<SQLRow>();
final SQLRowValues res = row.asRowValues();
try {
this.forDescendantsDo(row, new ChildProcessor<SQLRow>() {
public void process(SQLRow parent, SQLField joint, SQLRow desc) throws SQLException {
descsAndMe.add(desc);
this.forDescendantsDo(res, new ChildProcessor<SQLRowValues>() {
public void process(SQLRowValues parent, SQLField joint, SQLRowValues desc) throws SQLException {
desc.put(joint.getName(), parent);
}
}, true, true, archived);
}, true, false, archived);
} catch (SQLException e) {
// never happen cause process don't throw it
e.printStackTrace();
}
if (row.isArchived() == archived)
descsAndMe.add(row);
return descsAndMe;
return res;
}
 
/**
1447,14 → 1599,14
* @param row a SQLRow.
* @return the children rows by SQLTable.
*/
public CollectionMap<SQLTable, SQLRow> getChildrenRows(SQLRow row) {
public ListMap<SQLTable, SQLRow> getChildrenRows(SQLRow row) {
check(row);
// ArrayList
final CollectionMap<SQLTable, SQLRow> mm = new CollectionMap<SQLTable, SQLRow>();
// 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.put(child.getTable(), child);
mm.add(child.getTable(), child);
}
}, true, false);
} catch (SQLException e) {
1486,15 → 1638,77
return this.getParentForeignFieldName() == null ? null : row.getForeignRow(this.getParentForeignFieldName(), mode);
}
 
// {SQLField => List<SQLRow>}
CollectionMap<SQLField, SQLRow> getNonChildrenReferents(SQLRow row) {
public final SQLRowValues getPrivateParent(final SQLRowAccessor row, final boolean modifyParameter) {
return this.getPrivateParent(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 getPrivateParent(final SQLRowAccessor row, final boolean modifyParameter, final ArchiveMode archiveMode) {
check(row);
final CollectionMap<SQLField, SQLRow> mm = new CollectionMap<SQLField, SQLRow>();
final List<SQLField> refFields = new ArrayList<SQLField>(this.getPrivateParentReferentFields());
if (row.isUndefined() || refFields.size() == 0)
return null;
final ListIterator<SQLField> listIter = refFields.listIterator();
final List<String> selects = new ArrayList<String>(refFields.size());
while (listIter.hasNext()) {
final SQLField refField = listIter.next();
final SQLSelect sel = new SQLSelect(true).addSelect(refField.getTable().getKey()).addRawSelect(String.valueOf(listIter.previousIndex()), "fieldIndex");
sel.setArchivedPolicy(archiveMode);
sel.setWhere(new Where(refField, "=", row.getIDNumber()));
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 SQLField refField = refFields.get(((Number) idAndIndex[1]).intValue());
final SQLRowValues res = new SQLRowValues(refField.getTable()).setID((Number) idAndIndex[0]);
// first convert to SQLRow to avoid modifying the (graph of our) method parameter
res.put(refField.getName(), (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 #getPrivateParent(SQLRowAccessor, boolean, ArchiveMode)
*/
public final SQLRowValues getPrivateRoot(SQLRowAccessor row, final ArchiveMode archiveMode) {
SQLRowValues prev = null;
SQLRowValues res = getPrivateParent(row, true, archiveMode);
while (res != null) {
prev = res;
res = getElement(res.getTable()).getPrivateParent(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.putAll(refField, row.getReferentRows(refField));
mm.put(refField, row.getReferentRows(refField));
}
return mm;
}
1605,6 → 1819,8
 
@Override
public final boolean equals(Object obj) {
if (obj == this)
return true;
if (obj instanceof SQLElement) {
final SQLElement o = (SQLElement) obj;
// don't need to compare SQLField computed by initFF() & initRF() since they're function
1636,7 → 1852,7
public final void addComponentFactory(final String id, final ITransformer<Tuple2<SQLElement, String>, SQLComponent> t) {
if (t == null)
throw new NullPointerException();
this.components.put(id, t);
this.components.add(id, t);
}
 
public final void removeComponentFactory(final String id, final ITransformer<Tuple2<SQLElement, String>, SQLComponent> t) {
1645,12 → 1861,11
this.components.remove(id, t);
}
 
private final SQLComponent createComponent(final String id, final boolean defaultItem) {
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 = ((LinkedList<ITransformer<Tuple2<SQLElement, String>, SQLComponent>>) this.components.getNonNull(actualID))
.descendingIterator();
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)
1669,19 → 1884,38
* else factories for {@link #DEFAULT_COMP_ID} are executed.
*
* @param id the requested ID.
* @return the component or <code>null</code> if all factories return <code>null</code>.
* @return the component, never <code>null</code>.
* @throws IllegalStateException if no component is found.
*/
public final SQLComponent createComponent(final String id) {
SQLComponent res = this.createComponent(id, false);
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.createComponent(id, true);
res = this.createComponentFromFactory(id, true);
}
}
if (res != null)
res.setCode(id);
else if (required)
throw new IllegalStateException("No component for " + id);
return res;
}
 
1776,7 → 2010,7
if (!UserRightsManager.getCurrentUserRights().canDelete(getTable()))
throw new SQLException("forbidden");
final TreesOfSQLRows trees = TreesOfSQLRows.createFromIDs(this, ids);
final CollectionMap<SQLTable, SQLRowAccessor> descs = trees.getDescendantsByTable();
final Map<SQLTable, List<SQLRowAccessor>> descs = trees.getDescendantsByTable();
final SortedMap<SQLField, Integer> externRefs = trees.getExternReferencesCount();
final String confirmDelete = getTM().trA("sqlElement.confirmDelete");
final Map<String, Object> map = new HashMap<String, Object>();
1819,15 → 2053,12
}
}
 
@SuppressWarnings("rawtypes")
private final String toString(MultiMap descs) {
final List<String> l = new ArrayList<String>();
final Iterator iter = descs.keySet().iterator();
while (iter.hasNext()) {
final SQLTable t = (SQLTable) iter.next();
final Collection rows = (Collection) descs.get(t);
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(rows.size(), elem));
l.add(elemToString(e.getValue().size(), elem));
}
return CollectionUtils.join(l, "\n");
}
/trunk/OpenConcerto/src/org/openconcerto/sql/FieldExpander.java
28,6 → 28,10
import java.util.Map;
import java.util.Set;
 
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;
 
@ThreadSafe
public abstract class FieldExpander {
 
static private final FieldExpander EMPTY = new FieldExpander() {
42,7 → 46,9
}
 
// eg |TABLEAU.ID_OBSERVATION| -> [[DESIGNATION], []]
@GuardedBy("this")
private final Map<IFieldPath, List<FieldPath>> cache;
@GuardedBy("this")
private final Map<List<? extends IFieldPath>, List<Tuple2<Path, List<FieldPath>>>> cacheGroupBy;
 
public FieldExpander() {
55,7 → 61,7
* If for the same input expandOnce() will now return a different output, you have to call this
* method.
*/
protected void clearCache() {
protected synchronized void clearCache() {
this.cache.clear();
this.cacheGroupBy.clear();
}
64,7 → 70,7
 
protected abstract List<SQLField> expandOnce(SQLField field);
 
protected final List<FieldPath> expandOnce(IFieldPath field) {
protected synchronized final List<FieldPath> expandOnce(IFieldPath field) {
final List<SQLField> e = this.expandOnce(field.getField());
if (e == null)
return null;
98,7 → 104,7
* @param field le nom du champ à expandre, eg "SITE.ID_ETABLISSEMENT".
* @return les champs.
*/
public final List<FieldPath> expand(IFieldPath field) {
public synchronized final List<FieldPath> expand(IFieldPath field) {
// eg field == BATIMENT.ID_SITE
if (this.cache.containsKey(field)) {
return this.cache.get(field);
135,7 → 141,7
*
* @param vals the SQLRowValues to expand.
*/
public final void expand(SQLRowValues vals) {
public synchronized final void expand(SQLRowValues vals) {
final Set<SQLField> fks = vals.getTable().getForeignKeys();
for (final String fName : vals.getFields()) {
final SQLField ffield = vals.getTable().getField(fName);
163,7 → 169,7
* @return the complete expansion, eg [ [LOCAL.DESIGNATION], [BAT.DES], [SITE.DES, ADRESSE.CP],
* [ETABLISSEMENT.DES] ].
*/
public final List<Tuple2<Path, List<FieldPath>>> expandGroupBy(List<? extends IFieldPath> fieldsOrig) {
public synchronized final List<Tuple2<Path, List<FieldPath>>> expandGroupBy(List<? extends IFieldPath> fieldsOrig) {
if (this.cacheGroupBy.containsKey(fieldsOrig))
return this.cacheGroupBy.get(fieldsOrig);
 
/trunk/OpenConcerto/src/org/openconcerto/sql/translation/messages_fr.properties
110,6 → 110,7
sqlElement.noLinesDeleted=Aucune ligne effacée.
sqlElement.noLinesDeletedTitle=Information
sqlElement.linksWillBeCut=- {elementName__indefiniteNumeral} {count, plural, one {va perdre son champ} other {vont perdre leurs champs}} "{linkName}"
sqlElement.linkCantBeCut={rowDesc} ne peut perdre son champ "{fieldLabel}"
 
user.passwordsDontMatch=Les mots de passe ne correspondent pas
user.passwordsDontMatch.short=Confirmation incorrecte
/trunk/OpenConcerto/src/org/openconcerto/sql/translation/messages_en.properties
108,7 → 108,8
sqlElement.deleteRef=Do you{times, select, once {} other { REALLY}} want to delete {rowCount, plural, one {this row} other {these # rows}} and all linked ones ?
sqlElement.noLinesDeleted=No lines deleted.
sqlElement.noLinesDeletedTitle=Information
sqlElement.linksWillBeCut=- {elementName__indefiniteNumeral} {count, plural, one {will loose its} other {will loose their}} "{linkName}"
sqlElement.linksWillBeCut=- {elementName__indefiniteNumeral} {count, plural, one {will lose its} other {will lose their}} "{linkName}"
sqlElement.linkCantBeCut={rowDesc} cannot lose its field "{fieldLabel}"
 
user.passwordsDontMatch=Passwords don\u2019t match
user.passwordsDontMatch.short=Passwords don\u2019t match
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLField.java
17,28 → 17,36
package org.openconcerto.sql.model;
 
import static org.openconcerto.sql.model.SQLBase.quoteIdentifier;
import org.openconcerto.sql.Log;
import org.openconcerto.sql.model.graph.Link;
import org.openconcerto.sql.model.graph.Path;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.ExceptionUtils;
import org.openconcerto.utils.Value;
import org.openconcerto.xml.JDOMUtils;
import org.openconcerto.xml.XMLCodecUtils;
 
import java.math.BigDecimal;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;
 
import org.jdom.Element;
import org.jdom2.Element;
 
/**
* Un champ SQL. Pour obtenir une instance de cette classe il faut utiliser
121,7 → 129,9
// all following attributes guarded by "this"
private SQLType type;
private final Map<String, Object> metadata;
private Object defaultValue;
private String defaultValue;
@GuardedBy("this")
private Value<Object> parsedDefaultValue;
private Boolean nullable;
// from information_schema.COLUMNS
private final Map<String, Object> infoSchemaCols;
145,7 → 155,9
// pg_get_expr(adbin) (see 44.6. pg_attrdef), this sometimes result in
// <nextval('"Preventec_Common"."DISCIPLINE_ID_seq"'::regclass)> !=
// <nextval('"DISCIPLINE_ID_seq"'::regclass)>
this.defaultValue = metadata.get("COLUMN_DEF");
this.defaultValue = (String) metadata.get("COLUMN_DEF");
// don't parse now, as it might not be possible : i.e. function calls
this.parsedDefaultValue = null;
this.fullName = this.getTable().getName() + "." + this.getName();
this.nullable = nullableStr2Obj((String) metadata.get("IS_NULLABLE"));
 
159,6 → 171,7
this.type = f.type;
this.metadata = new HashMap<String, Object>(f.metadata);
this.defaultValue = f.defaultValue;
this.parsedDefaultValue = f.parsedDefaultValue;
this.fullName = f.fullName;
this.nullable = f.nullable;
this.infoSchemaCols = new HashMap<String, Object>(f.infoSchemaCols);
172,6 → 185,7
this.metadata.clear();
this.metadata.putAll(f.metadata);
this.defaultValue = f.defaultValue;
this.parsedDefaultValue = f.parsedDefaultValue;
this.nullable = f.nullable;
this.setColsFromInfoSchema(f.infoSchemaCols);
this.xml = f.xml;
250,8 → 264,10
final SQLSystem sys = getServer().getSQLSystem();
if (sys == SQLSystem.H2) {
final String name = (String) this.infoSchemaCols.get("SEQUENCE_NAME");
if (name != null)
return new SQLName(name);
if (name != null) {
// H2 doesn't provide the schema name, but requires it when altering a field
return new SQLName(getDBRoot().getName(), name);
}
} else if (sys == SQLSystem.POSTGRESQL) {
if (allowRequest) {
final String req = "SELECT pg_get_serial_sequence(" + getTable().getBase().quoteString(getTable().getSQLName().quote()) + ", " + getTable().getBase().quoteString(this.getName()) + ")";
258,8 → 274,8
final String name = (String) getDBSystemRoot().getDataSource().executeScalar(req);
if (name != null)
return SQLName.parse(name);
} else {
final String def = ((String) this.getDefaultValue()).trim();
} else if (this.getDefaultValue() != null) {
final String def = this.getDefaultValue().trim();
if (def.startsWith("nextval")) {
final Matcher matcher = SEQ_PATTERN.matcher(def);
if (matcher.matches()) {
273,11 → 289,61
return null;
}
 
public synchronized Object getDefaultValue() {
/**
* The SQL default value.
*
* @return the default value, e.g. <code>"1"</code> or <code>"'none'"</code>.
* @see DatabaseMetaData#getColumns(String, String, String, String)
*/
public synchronized String getDefaultValue() {
return this.defaultValue;
}
 
/**
* Try to parse the SQL {@link #getDefaultValue() default value}. Numbers are always parsed to
* {@link BigDecimal}.
*
* @return {@link Value#getNone()} if parsing failed, otherwise the parsed value.
*/
public synchronized Value<Object> getParsedDefaultValue() {
if (this.parsedDefaultValue == null) {
final Class<?> javaType = this.getType().getJavaType();
final String defaultVal = SQLSyntax.getNormalizedDefault(this);
try {
Object p = null;
if (defaultVal == null || defaultVal.trim().equalsIgnoreCase("null")) {
p = null;
} else if (String.class.isAssignableFrom(javaType)) {
// Strings can be encoded a lot of different ways, see SQLBase.quoteString()
if (defaultVal.charAt(0) == '\'' && defaultVal.indexOf('\\') == -1)
p = SQLBase.unquoteStringStd(defaultVal);
else
this.parsedDefaultValue = Value.getNone();
} else if (Number.class.isAssignableFrom(javaType)) {
p = new BigDecimal(defaultVal);
} else if (Boolean.class.isAssignableFrom(javaType)) {
p = Boolean.parseBoolean(defaultVal);
} else if (Timestamp.class.isAssignableFrom(javaType)) {
p = Timestamp.valueOf(SQLBase.unquoteStringStd(defaultVal));
} else if (Time.class.isAssignableFrom(javaType)) {
p = Time.valueOf(SQLBase.unquoteStringStd(defaultVal));
} else if (Date.class.isAssignableFrom(javaType)) {
p = java.sql.Date.valueOf(SQLBase.unquoteStringStd(defaultVal));
} else {
throw new IllegalStateException("Unsupported type " + this.getType());
}
if (this.parsedDefaultValue == null)
this.parsedDefaultValue = Value.<Object> getSome(p);
} catch (Exception e) {
Log.get().log(Level.FINE, "Couldn't parse " + this.defaultValue, e);
this.parsedDefaultValue = Value.getNone();
}
assert this.parsedDefaultValue != null;
}
return this.parsedDefaultValue;
}
 
/**
* Whether this field accepts NULL.
*
* @return <code>true</code> if it does, <code>false</code> if not, <code>null</code> if
/trunk/OpenConcerto/src/org/openconcerto/sql/model/Where.java
105,10 → 105,12
}
 
static public Where createRaw(String clause, FieldRef... refs) {
return new Where(clause, Arrays.asList(refs));
return createRaw(clause, Arrays.asList(refs));
}
 
static public Where createRaw(String clause, Collection<? extends FieldRef> refs) {
if (clause == null)
return null;
return new Where(clause, refs);
}
 
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLServer.java
58,6 → 58,14
}
};
 
static private final <T> IClosure<? super T> coalesce(IClosure<? super T> o1, IClosure<? super T> o2) {
// ternary operator makes Eclipse fail
if (o1 != null)
return o1;
else
return o2;
}
 
public static final DBSystemRoot create(final SQL_URL url) {
return create(url, Collections.<String> emptySet(), null);
}
95,7 → 103,7
return res;
}
 
public static final DBSystemRoot create(final SQL_URL url, final IClosure<DBSystemRoot> systemRootInit, final IClosure<SQLDataSource> dsInit) {
public static final DBSystemRoot create(final SQL_URL url, final IClosure<? super DBSystemRoot> systemRootInit, final IClosure<? super SQLDataSource> dsInit) {
return new SQLServer(url.getSystem(), url.getServerName(), null, url.getLogin(), url.getPass(), systemRootInit, dsInit).getSystemRoot(url.getSystemRootName());
}
 
105,7 → 113,7
private final SQLSystem system;
private final String login;
private final String pass;
private final IClosure<DBSystemRoot> systemRootInit;
private final IClosure<? super DBSystemRoot> systemRootInit;
@GuardedBy("baseMutex")
private CopyOnWriteMap<String, SQLBase> bases;
private Object baseMutex = new String("base mutex");
117,7 → 125,7
private SQLDataSource ds;
@GuardedBy("this")
private boolean dsSet;
private final IClosure<SQLDataSource> dsInit;
private final IClosure<? super SQLDataSource> dsInit;
private final ITransformer<String, String> urlTransf;
 
@GuardedBy("this")
148,7 → 156,7
* @param dsInit to initialize the datasource before any request (e.g. setting JDBC properties),
* must be thread-safe, can be <code>null</code>.
*/
public SQLServer(SQLSystem system, String host, String port, String login, String pass, IClosure<DBSystemRoot> systemRootInit, IClosure<SQLDataSource> dsInit) {
public SQLServer(SQLSystem system, String host, String port, String login, String pass, IClosure<? super DBSystemRoot> systemRootInit, IClosure<? super SQLDataSource> dsInit) {
super(null, host);
this.ds = null;
this.dsSet = false;
182,8 → 190,11
* Signal that this server and its descendants will not be used anymore.
*/
public final void destroy() {
synchronized (this.getTreeMutex()) {
if (!this.isDropped())
this.dropped();
}
}
 
@Override
protected void onDrop() {
283,7 → 294,7
// ignore tablesToRefresh if the base didn't exist (see
// SQLBase.assureAllTables())
// we already have the datasource, so login/pass aren't used
jdbcTables = createBase(cat, "", "", DSINIT_ERROR, readCache);
jdbcTables = createBase(cat, null, "", "", DSINIT_ERROR, readCache);
}
if (!jdbcTables.isEmpty())
res.put(cat, jdbcTables);
381,17 → 392,17
* <code>null</code> meaning take the server one.
* @return the corresponding base.
*/
public SQLBase getBase(String baseName, String login, String pass, IClosure<SQLDataSource> dsInit) {
return this.getBase(baseName, login, pass, dsInit, true);
public SQLBase getBase(String baseName, String login, String pass, IClosure<? super SQLDataSource> dsInit) {
return this.getBase(baseName, null, login, pass, dsInit, true);
}
 
public SQLBase getBase(String baseName, String login, String pass, IClosure<SQLDataSource> dsInit, boolean readCache) {
public SQLBase getBase(String baseName, final IClosure<? super DBSystemRoot> systemRootInit, String login, String pass, IClosure<? super SQLDataSource> dsInit, boolean readCache) {
if (this.getDBSystemRoot() != null)
throw new IllegalStateException("getBase(name, login, pass) should only be used for systems where SQLBase is DBSystemRoot");
synchronized (this.getTreeMutex()) {
SQLBase base = this.getBase(baseName);
if (base == null) {
this.createBase(baseName, login, pass, dsInit, readCache);
this.createBase(baseName, systemRootInit, login, pass, dsInit, readCache);
base = this.getBase(baseName);
}
return base;
398,16 → 409,17
}
}
 
private final TablesMap createBase(String baseName, String login, String pass, IClosure<SQLDataSource> dsInit, boolean readCache) {
private final TablesMap createBase(String baseName, IClosure<? super DBSystemRoot> systemRootInit, String login, String pass, IClosure<? super SQLDataSource> dsInit, boolean readCache) {
final DBSystemRoot sysRoot = this.getDBSystemRoot();
if (sysRoot != null && !sysRoot.createNode(this, baseName))
throw new IllegalStateException(baseName + " is filtered, you must add it to rootsToMap");
final SQLBase base = this.getSQLSystem().getSyntax().createBase(this, baseName, login == null ? this.login : login, pass == null ? this.pass : pass, dsInit != null ? dsInit : this.dsInit);
final SQLBase base = this.getSQLSystem().getSyntax()
.createBase(this, baseName, coalesce(systemRootInit, this.systemRootInit), login == null ? this.login : login, pass == null ? this.pass : pass, coalesce(dsInit, this.dsInit));
return this.putBase(baseName, base, readCache);
}
 
public final DBSystemRoot getSystemRoot(String name) {
return this.getSystemRoot(name, null, null, null);
return this.getSystemRoot(name, null, null, null, null);
}
 
/**
416,6 → 428,7
*
* @param name name of the system root, NOTE: for some systems the server is the systemRoot so
* <code>name</code> will be silently ignored.
* @param systemRootInit to initialize the {@link DBSystemRoot} before setting the data source.
* @param login the login, <code>null</code> means default.
* @param pass the password, <code>null</code> means default.
* @param dsInit to initialize the datasource before any request (eg setting jdbc properties),
423,10 → 436,10
* @return the corresponding systemRoot.
* @see #isSystemRootCreated(String)
*/
public final DBSystemRoot getSystemRoot(String name, String login, String pass, IClosure<SQLDataSource> dsInit) {
public final DBSystemRoot getSystemRoot(String name, final IClosure<? super DBSystemRoot> systemRootInit, String login, String pass, IClosure<? super SQLDataSource> dsInit) {
synchronized (this.getTreeMutex()) {
if (!this.isSystemRootCreated(name)) {
return this.createSystemRoot(name, login, pass, dsInit);
return this.createSystemRoot(name, systemRootInit, login, pass, dsInit);
} else {
final DBSystemRoot res;
final DBSystemRoot sysRoot = this.getDBSystemRoot();
440,15 → 453,15
}
}
 
private final DBSystemRoot createSystemRoot(String name, String login, String pass, IClosure<SQLDataSource> dsInit) {
private final DBSystemRoot createSystemRoot(String name, IClosure<? super DBSystemRoot> systemRootInit, String login, String pass, IClosure<? super SQLDataSource> dsInit) {
final DBSystemRoot res;
synchronized (this.getTreeMutex()) {
final DBSystemRoot sysRoot = this.getDBSystemRoot();
if (sysRoot != null) {
res = sysRoot;
res.setDS(login == null ? this.login : login, pass == null ? this.pass : pass, dsInit != null ? dsInit : this.dsInit);
res.setDS(coalesce(systemRootInit, this.systemRootInit), login == null ? this.login : login, pass == null ? this.pass : pass, coalesce(dsInit, this.dsInit));
} else {
res = this.getBase(name, login, pass, dsInit).getDBSystemRoot();
res = this.getBase(name, coalesce(systemRootInit, this.systemRootInit), login, pass, dsInit, true).getDBSystemRoot();
}
}
return res;
543,14 → 556,6
return this.system;
}
 
void init(DBSystemRoot systemRoot) {
if (this.systemRootInit != null)
// allow it to not be thread-safe
synchronized (this.systemRootInit) {
this.systemRootInit.executeChecked(systemRoot);
}
}
 
public final synchronized void setID(final String id) {
this.id = id;
}
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLRow.java
22,8 → 22,8
import org.openconcerto.sql.model.graph.Link.Direction;
import org.openconcerto.sql.model.graph.Path;
import org.openconcerto.sql.utils.ReOrder;
import org.openconcerto.utils.CollectionMap;
import org.openconcerto.utils.DecimalUtils;
import org.openconcerto.utils.ListMap;
 
import java.math.BigDecimal;
import java.sql.ResultSet;
141,7 → 141,13
m.put(colName, rs.getObject(i + 1));
}
 
return new SQLRow(table, m);
final Number id = getID(m, table, true);
// e.g. LEFT JOIN : missing values are null
if (id == null)
return null;
 
// pass already found ID
return new SQLRow(table, id, m);
}
 
/**
171,7 → 177,9
static final List<SQLRow> createListFromRS(SQLTable table, ResultSet rs, final List<String> names) throws SQLException {
final List<SQLRow> res = new ArrayList<SQLRow>();
while (rs.next()) {
res.add(createFromRS(table, rs, names));
final SQLRow row = createFromRS(table, rs, names);
if (row != null)
res.add(row);
}
return res;
}
209,17 → 217,31
* @throws IllegalArgumentException si values ne contient pas la clef de la table.
*/
public SQLRow(SQLTable table, Map<String, ?> values) {
this(table, getID(values, table));
this(table, null, values);
}
 
// allow to call getID() only once
private SQLRow(SQLTable table, final Number id, Map<String, ?> values) {
this(table, id == null ? getID(values, table, false) : id);
// faire une copie, sinon backdoor pour changer les valeurs sans qu'on s'en aperçoive
this.setValues(new HashMap<String, Object>(values));
}
 
private static Number getID(Map<String, ?> values, final SQLTable table) {
// return ID, must always be present but may be null if <code>nullAllowed</code>
private static Number getID(Map<String, ?> values, final SQLTable table, final boolean nullAllowed) {
final String keyName = table.getKey().getName();
if (!values.keySet().contains(keyName))
if (!values.containsKey(keyName))
throw new IllegalArgumentException(values + " does not contain the key of " + table);
return (Number) values.get(keyName);
final Object keyValue = values.get(keyName);
if (keyValue instanceof Number) {
return (Number) keyValue;
} else if (nullAllowed && keyValue == null) {
return null;
} else {
final String valS = keyValue == null ? "' is null" : "' isn't a Number : " + keyValue.getClass() + " " + keyValue;
throw new IllegalArgumentException("The value of '" + keyName + valS);
}
}
 
private Map<String, Object> getValues() {
if (!this.fetched)
293,22 → 315,6
}
 
/**
* Est ce que cette ligne est archivée.
*
* @return <code>true</code> si la ligne était archivée lors de son instanciation.
*/
public boolean isArchived() {
// si il n'y a pas de champs archive, elle n'est pas archivée
if (!this.getTable().isArchivable())
return false;
// TODO sortir archive == 1
if (this.getTable().getArchiveField().getType().getJavaType().equals(Boolean.class))
return this.getBoolean(this.getTable().getArchiveField().getName()) == Boolean.TRUE;
else
return this.getInt(this.getTable().getArchiveField().getName()) == 1;
}
 
/**
* Est ce que cette ligne existe et n'est pas archivée.
*
* @return <code>true</code> si cette ligne est valide.
414,9 → 420,9
}
 
@Override
public int getForeignID(String fieldName) {
public Number getForeignIDNumber(String fieldName) throws IllegalArgumentException {
final SQLRow foreignRow = this.getForeignRow(fieldName, SQLRowMode.NO_CHECK);
return foreignRow == null ? SQLRow.NONEXISTANT_ID : foreignRow.getID();
return foreignRow == null ? null : foreignRow.getIDNumber();
}
 
@Override
703,25 → 709,25
* @return a List of SQLRow that points to this.
*/
public final List<SQLRow> getReferentRows(Set<SQLTable> tables, ArchiveMode archived) {
return new ArrayList<SQLRow>(this.getReferentRowsByLink(tables, archived).values());
return new ArrayList<SQLRow>(this.getReferentRowsByLink(tables, archived).allValues());
}
 
public final CollectionMap<Link, SQLRow> getReferentRowsByLink() {
public final ListMap<Link, SQLRow> getReferentRowsByLink() {
return this.getReferentRowsByLink(null);
}
 
public final CollectionMap<Link, SQLRow> getReferentRowsByLink(Set<SQLTable> tables) {
public final ListMap<Link, SQLRow> getReferentRowsByLink(Set<SQLTable> tables) {
return this.getReferentRowsByLink(tables, SQLSelect.UNARCHIVED);
}
 
public final CollectionMap<Link, SQLRow> getReferentRowsByLink(Set<SQLTable> tables, ArchiveMode archived) {
// ArrayList since getReferentRows() is ordered
final CollectionMap<Link, SQLRow> res = new CollectionMap<Link, SQLRow>(new ArrayList<SQLRow>());
public final ListMap<Link, SQLRow> getReferentRowsByLink(Set<SQLTable> tables, ArchiveMode archived) {
// List since getReferentRows() is ordered
final ListMap<Link, SQLRow> res = new ListMap<Link, SQLRow>();
final Set<Link> links = this.getTable().getBase().getGraph().getReferentLinks(this.getTable());
for (final Link l : links) {
final SQLTable src = l.getSource();
if (tables == null || tables != null && tables.contains(src)) {
res.putAll(l, this.getReferentRows(l.getLabel(), archived));
res.addAll(l, this.getReferentRows(l.getLabel(), archived));
}
}
return res;
909,9 → 915,7
* @return a SQLRowValues on this SQLRow.
*/
public SQLRowValues createUpdateRow() {
final SQLRowValues res = new SQLRowValues(this.getTable());
res.loadAbsolutelyAll(this);
return res;
return new SQLRowValues(this.getTable(), this.getValues());
}
 
/**
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLInjector.java
13,13 → 13,15
package org.openconcerto.sql.model;
 
import org.openconcerto.sql.Configuration;
import org.openconcerto.sql.model.graph.SQLKey;
import org.openconcerto.sql.model.graph.TablesMap;
import org.openconcerto.sql.preferences.SQLPreferences;
import org.openconcerto.sql.utils.AlterTable;
import org.openconcerto.sql.utils.ChangeTable;
import org.openconcerto.sql.utils.SQLCreateTable;
import org.openconcerto.sql.view.list.SQLTableModelSource;
import org.openconcerto.sql.view.list.SQLTableModelSourceOnline;
import org.openconcerto.utils.StringUtils;
import org.openconcerto.utils.cc.ITransformer;
 
import java.sql.SQLException;
164,6 → 166,34
rowVals.put(field.getName(), value);
}
 
protected void transfertReference(SQLRowAccessor srcRow, SQLRowValues rowVals, String from, String to) {
 
String label = rowVals.getString(to);
if (label != null && label.trim().length() > 0) {
rowVals.put(to, label + ", " + srcRow.getString(from));
} else {
rowVals.put(to, srcRow.getString(from));
}
}
 
protected void transfertNumberReference(SQLRowAccessor srcRow, SQLRowValues rowVals, final SQLTable tableElementDestination, String refField) {
SQLPreferences prefs = new SQLPreferences(srcRow.getTable().getDBRoot());
 
if (prefs.getBoolean("TransfertRef", true)) {
String label = rowVals.getString("NOM");
if (label != null && label.trim().length() > 0) {
rowVals.put("NOM", label + ", " + srcRow.getString("NUMERO"));
} else {
rowVals.put("NOM", srcRow.getString("NUMERO"));
}
} else if (prefs.getBoolean("TransfertMultiRef", false)) {
SQLRowValues rowValsHeader = new SQLRowValues(UndefinedRowValuesCache.getInstance().getDefaultRowValues(tableElementDestination));
String elementName = StringUtils.firstUp(Configuration.getInstance().getDirectory().getElement(getSource()).getName().getVariant(org.openconcerto.utils.i18n.Grammar.SINGULAR));
rowValsHeader.put("NOM", elementName + " N° " + srcRow.getString("NUMERO"));
rowValsHeader.put(refField, rowVals);
}
}
 
public synchronized SQLRow insertFrom(final SQLRowAccessor srcRow) throws SQLException {
return createRowValuesFrom(Arrays.asList(srcRow)).insert();
}
384,16 → 414,12
* @throws SQLException
* */
public void addTransfert(int idFrom, int idTo) throws SQLException {
System.err.println("SQLInjector.addTransfert() " + idFrom + " -> " + idTo);
final SQLTable tableTransfert = getSource().getTable(getTableTranferName());
final SQLRowValues rowTransfer = new SQLRowValues(tableTransfert);
 
final Set<SQLField> foreignKeysSrc = tableTransfert.getForeignKeys(getSource());
final Set<SQLField> foreignKeysDest = tableTransfert.getForeignKeys(getDestination());
 
rowTransfer.put(foreignKeysSrc.iterator().next().getName(), idFrom);
rowTransfer.put(foreignKeysDest.iterator().next().getName(), idTo);
 
rowTransfer.commit();
 
}
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLSyntaxH2.java
15,6 → 15,8
 
import org.openconcerto.sql.model.SQLField.Properties;
import org.openconcerto.sql.model.graph.TablesMap;
import org.openconcerto.sql.utils.ChangeTable.ClauseType;
import org.openconcerto.utils.ListMap;
import org.openconcerto.utils.NetUtils;
import org.openconcerto.utils.Tuple2;
 
33,21 → 35,21
 
SQLSyntaxH2() {
super(SQLSystem.H2);
this.typeNames.putAll(Boolean.class, "boolean", "bool", "bit");
this.typeNames.putAll(Integer.class, "integer", "int", "int4", "mediumint");
this.typeNames.putAll(Byte.class, "tinyint");
this.typeNames.putAll(Short.class, "smallint", "int2");
this.typeNames.putAll(Long.class, "bigint", "int8");
this.typeNames.putAll(BigDecimal.class, "decimal", "numeric", "number");
this.typeNames.putAll(Float.class, "real");
this.typeNames.putAll(Double.class, "double precision", "float", "float4", "float8");
this.typeNames.putAll(Timestamp.class, "timestamp", "smalldatetime", "datetime");
this.typeNames.putAll(java.util.Date.class, "date");
this.typeNames.putAll(Blob.class, "blob", "tinyblob", "mediumblob", "longblob", "image",
this.typeNames.addAll(Boolean.class, "boolean", "bool", "bit");
this.typeNames.addAll(Integer.class, "integer", "int", "int4", "mediumint");
this.typeNames.addAll(Byte.class, "tinyint");
this.typeNames.addAll(Short.class, "smallint", "int2");
this.typeNames.addAll(Long.class, "bigint", "int8");
this.typeNames.addAll(BigDecimal.class, "decimal", "numeric", "number");
this.typeNames.addAll(Float.class, "real");
this.typeNames.addAll(Double.class, "double precision", "float", "float4", "float8");
this.typeNames.addAll(Timestamp.class, "timestamp", "smalldatetime", "datetime");
this.typeNames.addAll(java.util.Date.class, "date");
this.typeNames.addAll(Blob.class, "blob", "tinyblob", "mediumblob", "longblob", "image",
// byte[]
"bytea", "raw", "varbinary", "longvarbinary", "binary");
this.typeNames.putAll(Clob.class, "clob", "text", "tinytext", "mediumtext", "longtext");
this.typeNames.putAll(String.class, "varchar", "longvarchar", "char", "character", "CHARACTER VARYING");
this.typeNames.addAll(Clob.class, "clob", "text", "tinytext", "mediumtext", "longtext");
this.typeNames.addAll(String.class, "varchar", "longvarchar", "char", "character", "CHARACTER VARYING");
}
 
@Override
56,11 → 58,17
}
 
@Override
public int getMaximumVarCharLength() {
// http://www.h2database.com/html/datatypes.html#varchar_type
return Integer.MAX_VALUE;
}
 
@Override
public boolean isAuto(SQLField f) {
if (f.getDefaultValue() == null)
return false;
 
final String def = ((String) f.getDefaultValue()).toUpperCase();
final String def = f.getDefaultValue().toUpperCase();
// we used to use IDENTITY which translate to long
return (f.getType().getJavaType() == Integer.class || f.getType().getJavaType() == Long.class) && def.contains("NEXT VALUE") && def.contains("SYSTEM_SEQUENCE");
}
98,7 → 106,7
}
 
@Override
public List<String> getAlterField(SQLField f, Set<Properties> toAlter, String type, String defaultVal, Boolean nullable) {
public Map<ClauseType, List<String>> getAlterField(SQLField f, Set<Properties> toAlter, String type, String defaultVal, Boolean nullable) {
final List<String> res = new ArrayList<String>();
if (toAlter.contains(Properties.TYPE)) {
// MAYBE implement AlterTableAlterColumn.CHANGE_ONLY_TYPE
117,7 → 125,7
// e.g. ALTER COLUMN "VARCHAR" varchar(150) DEFAULT 'testAllProps' NULL
if (toAlter.contains(Properties.NULLABLE))
res.add(this.setNullable(f, nullable));
return res;
return ListMap.singleton(ClauseType.ALTER_COL, res);
}
 
@Override
132,7 → 140,7
 
@Override
public String transfDefaultJDBC2SQL(SQLField f) {
String res = (String) f.getDefaultValue();
String res = f.getDefaultValue();
if (res != null && f.getType().getJavaType() == String.class && res.trim().toUpperCase().startsWith("STRINGDECODE")) {
// MAYBE create an attribute with a mem h2 db, instead of using db of f
res = (String) f.getTable().getBase().getDataSource().executeScalar("CALL " + res);
247,7 → 255,7
public List<Map<String, Object>> getConstraints(SQLBase b, TablesMap tables) throws SQLException {
final String sel = "SELECT \"TABLE_SCHEMA\", \"TABLE_NAME\", \"CONSTRAINT_NAME\", \n"
//
+ "case \"CONSTRAINT_TYPE\" when 'REFERENTIAL' then 'FOREIGN KEY' else \"CONSTRAINT_TYPE\" end as \"CONSTRAINT_TYPE\", \"COLUMN_LIST\"\n"
+ "case \"CONSTRAINT_TYPE\" when 'REFERENTIAL' then 'FOREIGN KEY' else \"CONSTRAINT_TYPE\" end as \"CONSTRAINT_TYPE\", \"COLUMN_LIST\", \"CHECK_EXPRESSION\" AS \"DEFINITION\"\n"
//
+ "FROM INFORMATION_SCHEMA.CONSTRAINTS " + getTablesMapJoin(b, tables)
// where
266,4 → 274,11
public String getDropTrigger(Trigger t) {
return "DROP TRIGGER " + new SQLName(t.getTable().getSchema().getName(), t.getName()).quote();
}
 
@Override
public String getUpdate(SQLTable t, List<String> tables, Map<String, String> setPart) throws UnsupportedOperationException {
if (tables.size() > 0)
throw new UnsupportedOperationException();
return super.getUpdate(t, tables, setPart);
}
}
/trunk/OpenConcerto/src/org/openconcerto/sql/model/MySQLBase.java
22,8 → 22,8
 
private List<String> modes;
 
MySQLBase(SQLServer server, String name, String login, String pass, IClosure<SQLDataSource> dsInit) {
super(server, name, login, pass, dsInit);
MySQLBase(SQLServer server, String name, IClosure<? super DBSystemRoot> systemRootInit, String login, String pass, IClosure<? super SQLDataSource> dsInit) {
super(server, name, systemRootInit, login, pass, dsInit);
this.modes = null;
}
 
44,6 → 44,8
@Override
public final String quoteString(String s) {
final String res = super.quoteString(s);
if (s == null)
return res;
// ATTN if shouldEscape() return false (from global mode) but session mode is the opposite,
// then SQL can be injected :
// toto \'; drop table ;
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLType.java
30,18 → 30,24
import java.sql.Types;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
 
import org.jdom.Element;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;
 
import org.jdom2.Element;
 
/**
* The type of a SQL field. Allow one to convert a Java object to its sql serialization.
* The type of a SQL field. Allow one to convert a Java object to its SQL serialization.
*
* @see #toString(Object)
* @see #check(Object)
* @author Sylvain
*/
@ThreadSafe
public abstract class SQLType {
 
private static Class<?> getClass(int type, final int size) {
98,6 → 104,7
}
}
 
@GuardedBy("instances")
static private final Map<List<String>, SQLType> instances = new HashMap<List<String>, SQLType>();
 
// useful when no SQLBase is known
127,6 → 134,7
*/
public static SQLType get(final SQLBase base, final int type, final int size, Integer decDigits, final String typeName) {
final List<String> typeID = Arrays.asList(base == null ? null : base.getURL(), type + "", size + "", String.valueOf(decDigits), typeName);
synchronized (instances) {
SQLType res = instances.get(typeID);
if (res == null) {
final Class<?> clazz = getClass(type, size);
151,6 → 159,7
}
return res;
}
}
 
public static SQLType get(final SQLBase base, Element typeElement) {
int type = Integer.valueOf(typeElement.getAttributeValue("type")).intValue();
161,6 → 170,17
return get(base, type, size, decDigits, typeName);
}
 
static void remove(final SQLBase base) {
synchronized (instances) {
final Iterator<Entry<List<String>, SQLType>> iter = instances.entrySet().iterator();
while (iter.hasNext()) {
final Entry<List<String>, SQLType> e = iter.next();
if (e.getValue().getBase() == base)
iter.remove();
}
}
}
 
// *** instance
 
// a value from java.sql.Types
174,9 → 194,12
// the class this type accepts
private final Class<?> javaType;
 
@GuardedBy("this")
private SQLBase base;
@GuardedBy("this")
private SQLSyntax syntax;
 
@GuardedBy("this")
private String xml;
 
private SQLType(int type, int size, Integer decDigits, String typeName, Class<?> javaType) {
223,7 → 246,7
}
 
// TODO remove once quoteString() is in SQLSyntax
private final void setBase(SQLBase base) {
private synchronized final void setBase(SQLBase base) {
// set only once
assert this.base == null;
if (base != null) {
232,7 → 255,7
}
}
 
private final void setSyntax(SQLSyntax s) {
private synchronized final void setSyntax(SQLSyntax s) {
// set only once
assert this.syntax == null;
if (s != null) {
240,11 → 263,11
}
}
 
private final SQLBase getBase() {
private synchronized final SQLBase getBase() {
return this.base;
}
 
public final SQLSyntax getSyntax() {
public synchronized final SQLSyntax getSyntax() {
return this.syntax;
}
 
314,7 → 337,7
return "SQLType #" + this.getType() + "(" + this.getSize() + "," + this.getDecimalDigits() + "): " + this.getJavaType();
}
 
public final String toXML() {
public synchronized final String toXML() {
// this class is immutable and its instances shared so cache its XML
if (this.xml == null) {
final StringBuilder sb = new StringBuilder(128);
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLRowValuesListFetcher.java
15,10 → 15,11
 
import org.openconcerto.sql.model.SQLRowValues.CreateMode;
import org.openconcerto.sql.model.SQLRowValuesCluster.State;
import org.openconcerto.sql.model.SQLRowValuesCluster.WalkOptions;
import org.openconcerto.sql.model.graph.Link.Direction;
import org.openconcerto.sql.model.graph.Path;
import org.openconcerto.sql.model.graph.Step;
import org.openconcerto.utils.CollectionMap;
import org.openconcerto.utils.CollectionMap2Itf.ListMapItf;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.ListMap;
import org.openconcerto.utils.RTInterruptedException;
45,6 → 46,7
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
 
import org.apache.commons.dbutils.ResultSetHandler;
89,7 → 91,7
}, RecursionType.DEPTH_FIRST, Direction.REFERENT);
 
// find out needed grafts
final CollectionMap<Path, SQLRowValuesListFetcher> grafts = new CollectionMap<Path, SQLRowValuesListFetcher>();
final ListMap<Path, SQLRowValuesListFetcher> grafts = new ListMap<Path, SQLRowValuesListFetcher>();
graph.getGraph().walk(graph, null, new ITransformer<State<Object>, Object>() {
@Override
public Path transformChecked(State<Object> input) {
121,9 → 123,9
if (ungrafted == null || ungrafted.size() == 0) {
// i.e. only one referent and thus graft not necessary
assert rec.descendantPath.length() > 0;
grafts.put(pMinusLast, rec);
grafts.add(pMinusLast, rec);
} else {
grafts.putAll(pMinusLast, ungrafted);
grafts.addAll(pMinusLast, ungrafted);
}
}
throw new SQLRowValuesCluster.StopRecurseException().setCompletely(false);
131,7 → 133,7
}
return null;
}
}, RecursionType.BREADTH_FIRST, Direction.ANY, false);
}, new WalkOptions(Direction.ANY).setRecursionType(RecursionType.BREADTH_FIRST).setStartIncluded(false));
 
final Set<Path> refPaths = new HashSet<Path>(handledPaths.values());
// remove the main fetcher
160,7 → 162,7
res.setOrdered(ordered);
 
// now graft recursively created grafts
for (final Entry<Path, Collection<SQLRowValuesListFetcher>> e : grafts.entrySet()) {
for (final Entry<Path, ? extends Collection<SQLRowValuesListFetcher>> e : grafts.entrySet()) {
final Path graftPath = e.getKey();
final Path refPath = handledPaths.get(graftPath);
// can be grafted on the main fetcher or on the referent fetchers
194,16 → 196,23
}
return input.getAcc();
}
}, RecursionType.BREADTH_FIRST, Direction.REFERENT, true);
}, RecursionType.BREADTH_FIRST, Direction.REFERENT);
// since includeStart=true
assert res.get() != null;
return res.get();
}
 
static private final CollectionMap<Tuple2<Path, Number>, SQLRowValues> createCollectionMap() {
static private final ListMap<Tuple2<Path, Number>, SQLRowValues> createCollectionMap() {
// we need a List in merge()
return new CollectionMap<Tuple2<Path, Number>, SQLRowValues>(new ArrayList<SQLRowValues>(8));
return new ListMap<Tuple2<Path, Number>, SQLRowValues>() {
@Override
public List<SQLRowValues> createCollection(Collection<? extends SQLRowValues> v) {
final List<SQLRowValues> res = new ArrayList<SQLRowValues>(8);
res.addAll(v);
return res;
}
};
}
 
private final SQLRowValues graph;
private final Path descendantPath;
505,7 → 514,7
if (graftPlace == null)
throw new IllegalArgumentException("path doesn't exist: " + graftPath);
assert graftPath.getLast() == graftPlace.getTable();
if (other.getGraph().getForeigns().size() > 0)
if (other.getGraph().hasForeigns())
throw new IllegalArgumentException("shouldn't have foreign rows");
 
final Path descendantPath = computePath(other.getGraph());
565,6 → 574,24
}
 
/**
* Get all fetchers.
*
* @param includeSelf <code>true</code> to include <code>this</code> (with a <code>null</code>
* key).
* @return all instances indexed by the graft path.
*/
public final ListMapItf<Path, SQLRowValuesListFetcher> getFetchers(final boolean includeSelf) {
final ListMap<Path, SQLRowValuesListFetcher> res = new ListMap<Path, SQLRowValuesListFetcher>();
for (final Entry<Path, Map<Path, SQLRowValuesListFetcher>> e : this.grafts.entrySet()) {
assert e.getKey() != null;
res.putCollection(e.getKey(), e.getValue().values());
}
if (includeSelf)
res.add(null, this);
return ListMap.unmodifiableMap(res);
}
 
/**
* Get instances which fetch the {@link Path#getLast() last table} of the passed path. E.g.
* useful if you want to add a where to a join. This method is recursively called on
* {@link #getGrafts(Path) grafts} thus the returned paths may be fetched by grafts.
603,9 → 630,14
}
 
private final void addFields(final SQLSelect sel, final SQLRowValues vals, final String alias) {
for (final String fieldName : vals.getFields())
// put key first
final SQLField key = vals.getTable().getKey();
sel.addSelect(new AliasedField(key, alias));
for (final String fieldName : vals.getFields()) {
if (!fieldName.equals(key.getName()))
sel.addSelect(new AliasedField(vals.getTable().getField(fieldName), alias));
}
}
 
public final SQLSelect getReq() {
if (this.isFrozen())
634,8 → 666,9
input.getAcc().addJoin(joinType, new AliasedField(input.getFrom(), aliasPrev), alias);
}
 
} else
} else {
alias = null;
}
addFields(input.getAcc(), input.getCurrent(), alias);
 
return input.getAcc();
687,7 → 720,7
throw new SQLRowValuesCluster.StopRecurseException().setCompletely(false);
return transf.transformChecked(input);
}
}, RecursionType.BREADTH_FIRST, Direction.ANY, false);
}, new WalkOptions(Direction.ANY).setRecursionType(RecursionType.BREADTH_FIRST).setStartIncluded(false));
}
 
// models the graph, so that we don't have to walk it for each row
694,6 → 727,7
private static final class GraphNode {
private final SQLTable t;
private final int fieldCount;
private final int foreignCount;
private final int linkIndex;
private final Step from;
 
701,6 → 735,7
super();
this.t = input.getCurrent().getTable();
this.fieldCount = input.getCurrent().size();
this.foreignCount = input.getCurrent().getForeigns().size();
this.linkIndex = input.getAcc();
final int length = input.getPath().length();
this.from = length == 0 ? null : input.getPath().getStep(length - 1);
714,6 → 749,10
return this.fieldCount;
}
 
public final int getForeignCount() {
return this.foreignCount;
}
 
public final int getLinkIndex() {
return this.linkIndex;
}
785,16 → 824,32
final List<SQLRowValues> row = new ArrayList<SQLRowValues>(graphSize);
for (int i = 0; i < graphSize; i++) {
final GraphNode node = l.get(i);
final SQLRowValues creatingVals = new SQLRowValues(node.getTable());
if (i == 0)
final int stop = rsIndex + node.getFieldCount();
final SQLRowValues creatingVals;
// the PK is always first and it can only be null if there was no row, i.e. all
// other fields will be null.
final Object first = rs.getObject(rsIndex);
if (first == null) {
creatingVals = null;
// don't bother reading all nulls
rsIndex = stop;
} else {
// don't pass referent count as it can be fetched by a graft, or else
// several rows might later be merged (e.g. *BATIMENT* <- LOCAL has only one
// referent but all locals of a batiment will point to the same row)
creatingVals = new SQLRowValues(node.getTable(), node.getFieldCount(), node.getForeignCount(), -1);
put(creatingVals, rsIndex, first);
rsIndex++;
}
if (i == 0) {
if (creatingVals == null)
throw new IllegalStateException("Null primary row");
res.add(creatingVals);
}
 
final int stop = rsIndex + node.getFieldCount();
for (; rsIndex < stop; rsIndex++) {
try {
// -1 since rs starts at 1
// field names checked below
creatingVals.put(this.selectFields.get(rsIndex - 1), rs.getObject(rsIndex), false);
put(creatingVals, rsIndex, rs.getObject(rsIndex));
} catch (SQLException e) {
throw new IllegalStateException("unable to fill " + creatingVals, e);
}
814,16 → 869,6
if (nextToLink > 0)
futures.add(exec.submit(new Linker(l, rows, nextToLink, rowSize)));
 
// check field names only once since each row has the same fields
if (rowSize > 0) {
final List<SQLRowValues> firstRow = rows.get(0);
for (int i = 0; i < graphSize; i++) {
final SQLRowValues vals = firstRow.get(i);
if (!vals.getTable().getFieldsName().containsAll(vals.getFields()))
throw new IllegalStateException("field name error : " + vals.getFields() + " not in " + vals.getTable().getFieldsName());
}
}
 
// either link all rows, or...
if (nextToLink == 0)
link(l, rows, 0, rowSize);
840,6 → 885,12
return res;
}
 
protected void put(final SQLRowValues creatingVals, int rsIndex, final Object obj) {
// -1 since rs starts at 1
// field names checked only once when nodes are created
creatingVals.put(this.selectFields.get(rsIndex - 1), obj, false);
}
 
@Override
public int hashCode() {
final int prime = 31;
877,9 → 928,11
private final List<SQLRowValues> fetch(final boolean merge) {
final SQLSelect req = this.getReq();
// getName() would take 5% of ResultSetHandler.handle()
final List<String> selectFields = new ArrayList<String>(req.getSelectFields().size());
for (final SQLField f : req.getSelectFields())
selectFields.add(f.getName());
final List<FieldRef> selectFields = req.getSelectFields();
final int selectFieldsSize = selectFields.size();
final List<String> selectFieldsNames = new ArrayList<String>(selectFieldsSize);
for (final FieldRef f : selectFields)
selectFieldsNames.add(f.getField().getName());
final SQLTable table = getGraph().getTable();
 
// create a flat list of the graph nodes, we just need the table, field count and the index
887,20 → 940,39
// <LOCAL,2,0>, <BATIMENT,2,0>, <SITE,5,1>, <CPI,4,0>
final int graphSize = this.getGraph().getGraph().size();
final List<GraphNode> l = new ArrayList<GraphNode>(graphSize);
// check field names only once since each row has the same fields
final AtomicInteger fieldIndex = new AtomicInteger(0);
walk(0, new ITransformer<State<Integer>, Integer>() {
@Override
public Integer transformChecked(State<Integer> input) {
final int index = l.size();
l.add(new GraphNode(input));
final GraphNode node = new GraphNode(input);
final int stop = fieldIndex.get() + node.getFieldCount();
for (int i = fieldIndex.get(); i < stop; i++) {
if (i >= selectFieldsSize)
throw new IllegalStateException("Fields were removed from the select");
final FieldRef field = selectFields.get(i);
if (!node.getTable().equals(field.getTableRef().getTable()))
throw new IllegalStateException("Select field not in " + node + " : " + field);
}
fieldIndex.set(stop);
l.add(node);
// used by link index of GraphNode
return index;
}
});
// otherwise walk() would already have thrown an exception
assert fieldIndex.get() <= selectFieldsSize;
if (fieldIndex.get() != selectFieldsSize) {
throw new IllegalStateException("Fields have been added to the select (which is useless, since only fields specified by rows are returned) : "
+ selectFields.subList(fieldIndex.get(), selectFieldsSize));
}
assert l.size() == graphSize : "All nodes weren't explored once : " + l.size() + " != " + graphSize + "\n" + this.getGraph().printGraph();
 
// if we wanted to use the cache, we'd need to copy the returned list and its items (i.e.
// deepCopy()), since we modify them afterwards. Or perhaps include the code after this line
// into the result set handler.
final IResultSetHandler rsh = new IResultSetHandler(new RSH(selectFields, l), false);
final IResultSetHandler rsh = new IResultSetHandler(new RSH(selectFieldsNames, l), false);
@SuppressWarnings("unchecked")
final List<SQLRowValues> res = (List<SQLRowValues>) table.getBase().getDataSource().execute(req.asString(), rsh, false);
// e.g. list of batiment pointing to site
917,12 → 989,12
// CollectionMap since the same row can be in multiple index of merged, e.g. when
// fetching *BATIMENT* -> SITE each site will be repeated as many times as it has
// children and if we want their DOSSIER they must be grafted on each line.
final CollectionMap<Tuple2<Path, Number>, SQLRowValues> byRows = createCollectionMap();
final ListMap<Tuple2<Path, Number>, SQLRowValues> byRows = createCollectionMap();
for (final SQLRowValues vals : merged) {
// can be empty when grafting on optional row
for (final SQLRowValues graftPlaceVals : vals.followPath(graftPlace, CreateMode.CREATE_NONE, false)) {
ids.add(graftPlaceVals.getIDNumber());
byRows.put(Tuple2.create(mapPath, graftPlaceVals.getIDNumber()), graftPlaceVals);
byRows.add(Tuple2.create(mapPath, graftPlaceVals.getIDNumber()), graftPlaceVals);
}
}
assert ids.size() == byRows.size();
985,7 → 1057,7
final SQLRowValues creatingVals = row.get(nodeIndex);
// don't link empty values (LEFT JOIN produces rowValues filled with
// nulls) to the graph
if (creatingVals.hasID()) {
if (creatingVals != null) {
final SQLRowValues valsToFill;
final SQLRowValues valsToPut;
if (backwards) {
1045,12 → 1117,12
* @param descendantPath the path to merge.
* @return the merged and grafted values.
*/
private final List<SQLRowValues> merge(final List<SQLRowValues> tree, final List<SQLRowValues> graft, final CollectionMap<Tuple2<Path, Number>, SQLRowValues> graftPlaceRows, Path descendantPath) {
private final List<SQLRowValues> merge(final List<SQLRowValues> tree, final List<SQLRowValues> graft, final ListMap<Tuple2<Path, Number>, SQLRowValues> graftPlaceRows, Path descendantPath) {
final boolean isGraft = graftPlaceRows != null;
assert (tree != graft) == isGraft : "Trying to graft onto itself";
final List<SQLRowValues> res = isGraft ? tree : new ArrayList<SQLRowValues>();
// so that every graft is actually grafted onto the tree
final CollectionMap<Tuple2<Path, Number>, SQLRowValues> map = isGraft ? graftPlaceRows : createCollectionMap();
final ListMap<Tuple2<Path, Number>, SQLRowValues> map = isGraft ? graftPlaceRows : createCollectionMap();
 
final int stop = descendantPath.length();
for (final SQLRowValues v : graft) {
1063,12 → 1135,12
final Tuple2<Path, Number> row = Tuple2.create(subPath, desc.getIDNumber());
if (map.containsKey(row)) {
doAdd = false;
assert ((List<SQLRowValues>) map.getNonNull(row)).get(0).getFields().containsAll(desc.getFields()) : "Discarding an SQLRowValues with more fields : " + desc;
assert map.get(row).get(0).getFields().containsAll(desc.getFields()) : "Discarding an SQLRowValues with more fields : " + desc;
// previous being null can happen when 2 grafted paths share some steps at
// the start, e.g. SOURCE -> LOCAL and CPI -> LOCAL with a LOCAL having a
// SOURCE but no CPI
if (previous != null) {
final List<SQLRowValues> destinationRows = (List<SQLRowValues>) map.getNonNull(row);
final List<SQLRowValues> destinationRows = map.get(row);
final int destinationSize = destinationRows.size();
assert destinationSize > 0 : "Map contains row but have no corresponding value: " + row;
final String ffName = descendantPath.getSingleStep(i).getName();
1085,7 → 1157,7
if (descCopy != null) {
final Tuple2<Path, Number> rowCopy = Tuple2.create(descendantPath.subPath(0, k), descCopy.getIDNumber());
assert map.containsKey(rowCopy) : "Since we already iterated with i";
map.put(rowCopy, descCopy);
map.add(rowCopy, descCopy);
}
}
}
1093,7 → 1165,7
previous.put(ffName, destinationRows.get(0));
}
} else {
map.put(row, desc);
map.add(row, desc);
}
previous = desc;
}
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLSyntax.java
16,17 → 16,19
import static org.openconcerto.utils.CollectionUtils.join;
import org.openconcerto.sql.Log;
import org.openconcerto.sql.model.SQLField.Properties;
import org.openconcerto.sql.model.SQLTable.Index;
import org.openconcerto.sql.model.SQLTable.SQLIndex;
import org.openconcerto.sql.model.graph.Link.Rule;
import org.openconcerto.sql.model.graph.TablesMap;
import org.openconcerto.sql.utils.ChangeTable.ClauseType;
import org.openconcerto.sql.utils.ChangeTable.OutsideClause;
import org.openconcerto.utils.CollectionMap;
import org.openconcerto.sql.utils.SQLUtils;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.ListMap;
import org.openconcerto.utils.NetUtils;
import org.openconcerto.utils.StringUtils;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.utils.cc.IClosure;
import org.openconcerto.utils.cc.IPredicate;
import org.openconcerto.utils.cc.ITransformer;
 
import java.io.File;
44,7 → 46,6
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
71,8 → 72,14
static protected final String TS_EXTENDED_JAVA_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS000";
static protected final String TS_BASIC_JAVA_FORMAT = "yyyyMMdd'T'HHmmss.SSS000";
 
static private final StringUtils.Escaper DEFAULT_LIKE_ESCAPER = new StringUtils.Escaper('\\', '\\');
 
static public enum ConstraintType {
CHECK, FOREIGN_KEY("FOREIGN KEY"), PRIMARY_KEY("PRIMARY KEY"), UNIQUE;
CHECK, FOREIGN_KEY("FOREIGN KEY"), PRIMARY_KEY("PRIMARY KEY"), UNIQUE,
/**
* Only used by MS SQL.
*/
DEFAULT;
 
private final String sqlName;
 
101,6 → 108,9
register(new SQLSyntaxH2());
register(new SQLSyntaxMySQL());
register(new SQLSyntaxMS());
 
DEFAULT_LIKE_ESCAPER.add('_', '_');
DEFAULT_LIKE_ESCAPER.add('%', '%');
}
 
static private void register(SQLSyntax s) {
120,21 → 130,22
}
 
private final SQLSystem sys;
protected final CollectionMap<Class, String> typeNames;
// list to specify the preferred first
protected final ListMap<Class<?>, String> typeNames;
 
protected SQLSyntax(final SQLSystem sys) {
this.sys = sys;
this.typeNames = new CollectionMap<Class, String>(new HashSet<String>(4));
this.typeNames = new ListMap<Class<?>, String>();
}
 
/**
* The set of aliases for a particular type.
* The aliases for a particular type. The first one is the preferred.
*
* @param clazz the type, e.g. Integer.class.
* @return the SQL aliases, e.g. {"integer", "int", "int4"}.
*/
public final Set<String> getTypeNames(Class clazz) {
return (Set<String>) this.typeNames.getNonNull(clazz);
public final Collection<String> getTypeNames(Class<?> clazz) {
return this.typeNames.getNonNull(clazz);
}
 
public final SQLSystem getSystem() {
141,6 → 152,11
return this.sys;
}
 
public String getInitSystemRoot() {
// by default: nothing
return "";
}
 
public String getInitRoot(final String name) {
// by default: nothing
return "";
206,7 → 222,7
return false;
// do not check UNIQUE since it might require re-order
 
return f.isNullable() && f.getDefaultValue() == getOrderDefault();
return f.isNullable() && CompareUtils.equals(f.getDefaultValue(), getOrderDefault());
}
 
public final String getOrderDefinition() {
264,17 → 280,7
return res + " (" + quoteIdentifiers(fields) + ");";
}
 
public List<OutsideClause> getCreateIndexes(final SQLTable t, final IPredicate<Index> pred) throws SQLException {
final List<Index> indexes = t.getIndexes();
final List<OutsideClause> res = new ArrayList<OutsideClause>(indexes.size());
for (final Index i : indexes) {
if (pred == null || pred.evaluateChecked(i))
res.add(getCreateIndex(i));
}
return res;
}
 
public final OutsideClause getCreateIndex(final Index i) {
public final OutsideClause getCreateIndex(final SQLIndex i) {
return new OutsideClause() {
 
@Override
289,27 → 295,18
// tablename
final String indexName = getSchemaUniqueName(tableName.getName(), i.getName());
String res = "CREATE" + (i.isUnique() ? " UNIQUE" : "") + " INDEX " + SQLBase.quoteIdentifier(indexName) + " ";
final String exprs = join(i.getAttrs(), ", ", new ITransformer<String, String>() {
@Override
public String transformChecked(String attr) {
if (i.getTable().contains(attr))
return SQLBase.quoteIdentifier(attr);
else
// eg lower("field")
return attr;
}
});
final String exprs = join(i.getAttrs(), ", ");
res += getCreateIndex("(" + exprs + ")", tableName, i);
// filter condition or warning if this doesn't support it
final boolean supported;
final boolean neededButUnsupported;
if (i.getFilter() != null && i.getFilter().length() > 0) {
res += " WHERE " + i.getFilter();
supported = getSystem().isIndexFilterConditionSupported();
neededButUnsupported = !getSystem().isIndexFilterConditionSupported();
} else {
supported = true;
neededButUnsupported = false;
}
res += ";";
if (!supported) {
if (neededButUnsupported) {
res = "-- filter condition not supported\n-- " + res;
Log.get().warning(res);
}
327,10 → 324,14
* @param i the index, do not use its table, use <code>tableName</code>.
* @return the part after "CREATE UNIQUE INDEX foo ".
*/
protected String getCreateIndex(final String cols, final SQLName tableName, Index i) {
protected String getCreateIndex(final String cols, final SQLName tableName, SQLIndex i) {
return "ON " + tableName.quote() + cols;
}
 
public boolean isUniqueException(final SQLException exn) {
return SQLUtils.findWithSQLState(exn).getSQLState().equals("23505");
}
 
/**
* Something to be appended to CREATE TABLE statements, like "ENGINE = InnoDB".
*
379,11 → 380,18
}
 
protected final String getDefault(SQLField f, final String sqlType) {
if (!this.supportsDefault(sqlType))
return null;
final String stdDefault = getNormalizedDefault(f);
return stdDefault == null ? null : this.transfDefault(f, stdDefault);
}
 
static final String getNormalizedDefault(SQLField f) {
final SQLSyntax fs = f.getServer().getSQLSystem().getSyntax();
final String stdDefault = fs.transfDefaultSQL2Common(f);
if (stdDefault == null || !this.supportsDefault(sqlType))
if (stdDefault == null) {
return null;
else {
} else {
// for the field date default '2008-12-30'
// pg will report a default value of '2008-12-30'::date
// for the field date default '2008-12-30'::date
395,7 → 403,7
castless = stdDefault;
else
castless = remove(stdDefault, fs.getTypeNames(f.getType().getJavaType()), cast.get0(), cast.get1());
return this.transfDefault(f, castless);
return castless;
}
}
 
496,7 → 504,7
if (f.getDefaultValue() == null)
return false;
 
final String def = ((String) f.getDefaultValue()).toLowerCase();
final String def = getNormalizedDefault(f).toLowerCase();
return Date.class.isAssignableFrom(f.getType().getJavaType()) && (def.contains("now") || def.contains("current_"));
}
 
525,6 → 533,14
return "boolean";
}
 
/**
* The maximum number of characters in a column. Can be less than that if there are other
* columns.
*
* @return the maximum number of characters.
*/
public abstract int getMaximumVarCharLength();
 
protected boolean supportsDefault(String sqlType) {
return true;
}
541,7 → 557,7
return castless;
}
 
private static final Set<String> nonStandardTimeFunctions = CollectionUtils.createSet("now()", "transaction_timestamp()", "current_timestamp()");
private static final Set<String> nonStandardTimeFunctions = CollectionUtils.createSet("now()", "transaction_timestamp()", "current_timestamp()", "getdate()");
/** list of columns identifying a field in the resultSet from information_schema.COLUMNS */
public static final List<String> INFO_SCHEMA_NAMES_KEYS = Arrays.asList("TABLE_SCHEMA", "TABLE_NAME", "COLUMN_NAME");
 
557,7 → 573,7
}
 
public String transfDefaultJDBC2SQL(SQLField f) {
return (String) f.getDefaultValue();
return f.getDefaultValue();
}
 
/**
639,9 → 655,9
* @param toTake which properties of <code>from</code> to copy.
* @return the SQL clauses.
*/
public final List<String> getAlterField(SQLField f, SQLField from, Set<Properties> toTake) {
public final Map<ClauseType, List<String>> getAlterField(SQLField f, SQLField from, Set<Properties> toTake) {
if (toTake.size() == 0)
return Collections.emptyList();
return Collections.emptyMap();
 
final Boolean nullable = toTake.contains(Properties.NULLABLE) ? getNullable(from) : null;
final String newType;
658,7 → 674,7
}
 
// cannot rename since some systems won't allow it in the same ALTER TABLE
public abstract List<String> getAlterField(SQLField f, Set<Properties> toAlter, String type, String defaultVal, Boolean nullable);
public abstract Map<ClauseType, List<String>> getAlterField(SQLField f, Set<Properties> toAlter, String type, String defaultVal, Boolean nullable);
 
/**
* The decimal, arbitrary precision, SQL type.
685,6 → 701,22
return getDecimal(intPart + fractionalPart, fractionalPart);
}
 
/**
* Rename a table. Some systems (e.g. MS SQL) need another query to change schema so this method
* doesn't support it.
*
* @param table the table to rename.
* @param newName the new name.
* @return the SQL statement.
*/
public String getRenameTable(SQLName table, String newName) {
return "ALTER TABLE " + table.quote() + " RENAME to " + SQLBase.quoteIdentifier(newName);
}
 
public String getDropTableIfExists(SQLName name) {
return "DROP TABLE IF EXISTS " + name.quote();
}
 
public abstract String getDropRoot(String name);
 
public abstract String getCreateRoot(String name);
760,13 → 792,13
*
* @param r the root to dump.
* @param dir where to dump it.
* @throws IllegalArgumentException if the server and this jvm aren't on the same machine.
* @throws IOException if an error occurred.
*/
public final void storeData(final DBRoot r, final File dir) {
public final void storeData(final DBRoot r, final File dir) throws IOException {
this.storeData(r, null, dir);
}
 
public final void storeData(final DBRoot r, final Set<String> tableNames, final File dir) {
public final void storeData(final DBRoot r, final Set<String> tableNames, final File dir) throws IOException {
dir.mkdirs();
final Map<String, SQLTable> tables = new TreeMap<String, SQLTable>(r.getTablesMap());
if (tableNames != null)
776,11 → 808,11
}
}
 
public final void storeData(SQLTable t, File f) {
public final void storeData(SQLTable t, File f) throws IOException {
this._storeData(t, f);
}
 
protected abstract void _storeData(SQLTable t, File f);
protected abstract void _storeData(SQLTable t, File f) throws IOException;
 
/**
* Whether the passed server runs on this machine.
797,8 → 829,8
throw new IllegalArgumentException("the server of " + t + " is not this computer: " + t.getServer());
}
 
SQLBase createBase(SQLServer server, String name, String login, String pass, IClosure<SQLDataSource> dsInit) {
return new SQLBase(server, name, login, pass, dsInit);
SQLBase createBase(SQLServer server, String name, final IClosure<? super DBSystemRoot> systemRootInit, String login, String pass, IClosure<? super SQLDataSource> dsInit) {
return new SQLBase(server, name, systemRootInit, login, pass, dsInit);
}
 
/**
820,6 → 852,10
return "||";
}
 
public String getLitteralLikePattern(final String pattern) {
return DEFAULT_LIKE_ESCAPER.escape(pattern);
}
 
public final String getRegexpOp() {
return this.getRegexpOp(false);
}
1012,7 → 1048,7
* @param b the base.
* @param tables the tables by schemas names.
* @return a list of map with at least "TABLE_SCHEMA", "TABLE_NAME", "CONSTRAINT_NAME",
* "CONSTRAINT_TYPE" and (List of String)"COLUMN_NAMES" keys.
* "CONSTRAINT_TYPE", (List of String)"COLUMN_NAMES" keys and "DEFINITION".
* @throws SQLException if an error occurs.
*/
public abstract List<Map<String, Object>> getConstraints(SQLBase b, TablesMap tables) throws SQLException;
1041,8 → 1077,8
 
/**
* A query to retrieve triggers in the passed schemas and tables. The result must have at least
* TRIGGER_NAME, TABLE_SCHEMA, TABLE_NAME, ACTION (system dependant, eg "NEW.F = true") and SQL
* (the SQL needed to create the trigger, can be <code>null</code>).
* TRIGGER_NAME, TABLE_SCHEMA, TABLE_NAME, ACTION (system dependent, e.g. "NEW.F = true") and
* SQL (the SQL needed to create the trigger, can be <code>null</code>).
*
* @param b the base.
* @param tables the tables by schemas names.
1060,18 → 1096,20
* @param tables the other tables of the update.
* @param setPart the fields of <code>t</code> and their values.
* @return the SQL specifying how to set the fields.
* @throws UnsupportedOperationException if this system doesn't support the passed update, eg
* @throws UnsupportedOperationException if this system doesn't support the passed update, e.g.
* multi-table.
*/
public String getUpdate(SQLTable t, List<String> tables, Map<String, String> setPart) throws UnsupportedOperationException {
if (tables.size() > 0)
throw new UnsupportedOperationException();
return t.getSQLName() + "\nSET " + CollectionUtils.join(setPart.entrySet(), ",\n", new ITransformer<Entry<String, String>, String>() {
String res = t.getSQLName().quote() + " SET\n" + CollectionUtils.join(setPart.entrySet(), ",\n", new ITransformer<Entry<String, String>, String>() {
@Override
public String transformChecked(Entry<String, String> input) {
return input.getKey() + " = " + input.getValue();
// pg require that fields are unprefixed
return SQLBase.quoteIdentifier(input.getKey()) + " = " + input.getValue();
}
});
if (tables.size() > 0)
res += " FROM " + CollectionUtils.join(tables, ", ");
return res;
}
 
public OutsideClause getSetTableComment(final String comment) {
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLSyntaxMS.java
14,19 → 14,39
package org.openconcerto.sql.model;
 
import org.openconcerto.sql.model.SQLField.Properties;
import org.openconcerto.sql.model.graph.Link.Rule;
import org.openconcerto.sql.model.graph.TablesMap;
import org.openconcerto.sql.utils.ChangeTable.ClauseType;
import org.openconcerto.sql.utils.ChangeTable.OutsideClause;
import org.openconcerto.sql.utils.SQLUtils;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.FileUtils;
import org.openconcerto.utils.ListMap;
import org.openconcerto.utils.ProcessStreams;
import org.openconcerto.utils.ProcessStreams.Action;
import org.openconcerto.utils.RTInterruptedException;
import org.openconcerto.utils.StringUtils;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.utils.cc.IClosure;
import org.openconcerto.utils.cc.ITransformer;
 
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.sql.Types;
import java.util.Arrays;
import java.util.BitSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
36,29 → 56,43
 
SQLSyntaxMS() {
super(SQLSystem.MSSQL);
this.typeNames.putAll(Boolean.class, "bit");
this.typeNames.putAll(Short.class, "smallint");
this.typeNames.putAll(Integer.class, "unsigned smallint", "int");
this.typeNames.putAll(Long.class, "unsigned int", "bigint");
this.typeNames.putAll(BigDecimal.class, "unsigned bigint", "decimal", "numeric", "smallmoney", "money");
this.typeNames.putAll(Float.class, "real");
this.typeNames.putAll(Double.class, "float");
this.typeNames.putAll(Timestamp.class, "smalldatetime", "datetime");
this.typeNames.putAll(java.sql.Date.class, "date");
this.typeNames.putAll(java.sql.Time.class, "time");
this.typeNames.putAll(Blob.class, "image",
this.typeNames.addAll(Boolean.class, "bit");
// tinyint is unsigned
this.typeNames.addAll(Short.class, "smallint", "tinyint");
this.typeNames.addAll(Integer.class, "int");
this.typeNames.addAll(Long.class, "bigint");
this.typeNames.addAll(BigDecimal.class, "decimal", "numeric", "smallmoney", "money");
this.typeNames.addAll(Float.class, "real");
this.typeNames.addAll(Double.class, "float");
this.typeNames.addAll(Timestamp.class, "smalldatetime", "datetime");
this.typeNames.addAll(java.sql.Date.class, "date");
this.typeNames.addAll(java.sql.Time.class, "time");
this.typeNames.addAll(Blob.class, "image",
// byte[]
"varbinary", "binary");
this.typeNames.putAll(Clob.class, "text", "ntext", "unitext");
this.typeNames.putAll(String.class, "char", "varchar", "nchar", "nvarchar", "unichar", "univarchar");
this.typeNames.addAll(Clob.class, "text", "ntext", "unitext");
this.typeNames.addAll(String.class, "char", "varchar", "nchar", "nvarchar", "unichar", "univarchar");
}
 
@Override
SQLBase createBase(SQLServer server, String name, final IClosure<? super DBSystemRoot> systemRootInit, String login, String pass, IClosure<? super SQLDataSource> dsInit) {
return new MSSQLBase(server, name, systemRootInit, login, pass, dsInit);
}
 
@Override
public String getInitSystemRoot() {
final String sql;
try {
final String fileContent = FileUtils.readUTF8(SQLSyntaxPG.class.getResourceAsStream("mssql-functions.sql"));
sql = fileContent.replace("${rootName}", SQLBase.quoteIdentifier("dbo"));
} catch (IOException e) {
throw new IllegalStateException("cannot read functions", e);
}
return sql;
}
 
@Override
public boolean isAuto(SQLField f) {
// FIXME test
if (f.getDefaultValue() == null)
return false;
 
return f.getType().getJavaType() == Integer.class && "YES".equals(f.getMetadata("IS_AUTOINCREMENT"));
}
 
78,6 → 112,12
}
 
@Override
public int getMaximumVarCharLength() {
// http://msdn.microsoft.com/en-us/library/ms176089(v=sql.105).aspx
return 8000;
}
 
@Override
public String transfDefaultJDBC2SQL(SQLField f) {
final Object def = f.getDefaultValue();
if (def == null)
105,6 → 145,12
}
 
@Override
protected String getRuleSQL(final Rule r) {
// MSSQL doesn't support RESTRICT
return (r.equals(Rule.RESTRICT) ? Rule.NO_ACTION : r).asString();
}
 
@Override
public String disableFKChecks(DBRoot b) {
return fkChecks(b, false);
}
121,6 → 167,33
return fkChecks(b, true);
}
 
@Override
public List<Map<String, Object>> getIndexInfo(SQLTable t) throws SQLException {
final String query = "SELECT NULL AS \"TABLE_CAT\", schema_name(t.schema_id) as \"TABLE_SCHEM\", t.name as \"TABLE_NAME\",\n" +
//
"~idx.is_unique as \"NON_UNIQUE\", NULL AS \"INDEX_QUALIFIER\", idx.name as \"INDEX_NAME\", NULL as \"TYPE\",\n" +
//
"indexCols.key_ordinal as \"ORDINAL_POSITION\", cols.name as \"COLUMN_NAME\",\n" +
//
"case when indexCols.is_descending_key = 1 then 'D' else 'A' end as \"ASC_OR_DESC\", null as \"CARDINALITY\", null as \"PAGES\",\n" +
//
"filter_definition as \"FILTER_CONDITION\"\n" +
//
" FROM [test].[sys].[objects] t\n" +
//
" join [test].[sys].[indexes] idx on idx.object_id = t.object_id\n" +
//
" join [test].[sys].[index_columns] indexCols on idx.index_id = indexCols.index_id and idx.object_id = indexCols.object_id\n" +
//
" join [test].[sys].[columns] cols on t.object_id = cols.object_id and cols.column_id = indexCols.column_id \n" +
//
" where schema_name(t.schema_id) = " + t.getBase().quoteString(t.getSchema().getName()) + " and t.name = " + t.getBase().quoteString(t.getName()) + "\n"
//
+ "ORDER BY \"NON_UNIQUE\", \"TYPE\", \"INDEX_NAME\", \"ORDINAL_POSITION\";";
// don't cache since we don't listen on system tables
return (List<Map<String, Object>>) t.getDBSystemRoot().getDataSource().execute(query, new IResultSetHandler(SQLDataSource.MAP_LIST_HANDLER, false));
}
 
@SuppressWarnings("unchecked")
@Override
public Map<String, Object> normalizeIndexInfo(final Map m) {
135,31 → 208,45
return "DROP INDEX " + SQLBase.quoteIdentifier(name) + " on " + tableName.quote() + ";";
}
 
protected String setNullable(SQLField f, boolean b) {
return "ALTER COLUMN " + f.getQuotedName() + " SET " + (b ? "" : "NOT") + " NULL";
@Override
public boolean isUniqueException(SQLException exn) {
return SQLUtils.findWithSQLState(exn).getErrorCode() == 2601;
}
 
// FIXME
@Override
public List<String> getAlterField(SQLField f, Set<Properties> toAlter, String type, String defaultVal, Boolean nullable) {
final List<String> res = new ArrayList<String>();
if (toAlter.contains(Properties.TYPE)) {
// MAYBE implement AlterTableAlterColumn.CHANGE_ONLY_TYPE
final String newDef = toAlter.contains(Properties.DEFAULT) ? defaultVal : getDefault(f, type);
public Map<ClauseType, List<String>> getAlterField(SQLField f, Set<Properties> toAlter, String type, String defaultVal, Boolean nullable) {
final ListMap<ClauseType, String> res = new ListMap<ClauseType, String>();
if (toAlter.contains(Properties.TYPE) || toAlter.contains(Properties.NULLABLE)) {
final String newType = toAlter.contains(Properties.TYPE) ? type : getType(f);
final boolean newNullable = toAlter.contains(Properties.NULLABLE) ? nullable : getNullable(f);
res.add("ALTER COLUMN " + f.getQuotedName() + " " + getFieldDecl(type, newDef, newNullable));
} else {
if (toAlter.contains(Properties.NULLABLE))
res.add(this.setNullable(f, nullable));
if (toAlter.contains(Properties.DEFAULT))
res.add(this.setDefault(f, defaultVal));
res.add(ClauseType.ALTER_COL, "ALTER COLUMN " + f.getQuotedName() + " " + getFieldDecl(newType, null, newNullable));
}
if (toAlter.contains(Properties.DEFAULT)) {
final Constraint existingConstraint = f.getTable().getConstraint(ConstraintType.DEFAULT, Arrays.asList(f.getName()));
if (existingConstraint != null) {
res.add(ClauseType.DROP_CONSTRAINT, "DROP CONSTRAINT " + SQLBase.quoteIdentifier(existingConstraint.getName()));
}
if (defaultVal != null) {
res.add(ClauseType.ADD_CONSTRAINT, "ADD DEFAULT " + defaultVal + " FOR " + f.getQuotedName());
}
}
return res;
}
 
@Override
public String getRenameTable(SQLName table, String newName) {
return "sp_rename " + SQLBase.quoteStringStd(table.quote()) + ", " + SQLBase.quoteStringStd(newName);
}
 
@Override
public String getDropTableIfExists(SQLName name) {
final String quoted = name.quote();
return "IF OBJECT_ID(" + SQLBase.quoteStringStd(quoted) + ", 'U') IS NOT NULL DROP TABLE " + quoted;
}
 
@Override
public String getDropRoot(String name) {
// FIXME
// Only works if getInitSystemRoot() was executed
// http://ranjithk.com/2010/01/31/script-to-drop-all-objects-of-a-schema/
return "exec CleanUpSchema " + SQLBase.quoteStringStd(name) + ", 'w' ;";
}
174,83 → 261,199
return null;
}
 
private static final Pattern nullPatrn = Pattern.compile("\\N", Pattern.LITERAL);
private static final Pattern backSlashPatrn = Pattern.compile("\\\"", Pattern.LITERAL);
private static final Pattern newlinePatrn = Pattern.compile("\n");
private static final Pattern newlineAndIDPatrn = Pattern.compile("\n(?=\\p{Digit}+\\|)");
 
private static final Pattern commaSepPatrn = Pattern.compile("(?<!\\\\)\",\"");
private static final Pattern firstLastQuotePatrn = Pattern.compile("(^\")|(\"$)", Pattern.MULTILINE);
 
// zero-width lookbehind to handle sequential boolean
private static final Pattern boolTPatrn = Pattern.compile("(?<=\\|)t\\|");
private static final Pattern boolFPatrn = Pattern.compile("(?<=\\|)f\\|");
private static final Pattern boolTEndPatrn = Pattern.compile("\\|t$", Pattern.MULTILINE);
private static final Pattern boolFEndPatrn = Pattern.compile("\\|f$", Pattern.MULTILINE);
 
// 2007-12-21 10:39:09.031+01 with microseconds part being variable length and optional
private static final Pattern dateWithOffsetPatrn = Pattern.compile("(\\|\\p{Digit}{4}-\\p{Digit}{2}-\\p{Digit}{2} \\p{Digit}{2}:\\p{Digit}{2}:\\p{Digit}{2}(.\\p{Digit}{1,3})?)\\+\\p{Digit}{2}");
 
@Override
public void _loadData(final File f, final SQLTable t) throws IOException {
// FIXME null handling ?
final String data = FileUtils.read(f, "UTF-8");
final String sansNull = nullPatrn.matcher(data).replaceAll("\"\"");
final String data = FileUtils.readUTF8(f);
final File temp = File.createTempFile(FileUtils.sanitize("mssql_loadData_" + t.getName()), ".txt");
 
String tmp = sansNull;
// no we cant't use UTF16 since Java write BE and MS ignores the BOM, always using LE.
final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(temp), Charset.forName("x-UTF-16LE-BOM")));
 
// remove header with column names
tmp = tmp.substring(tmp.indexOf('\n') + 1, tmp.length());
final List<SQLField> fields = t.getOrderedFields();
final int fieldsCount = fields.size();
final BitSet booleanFields = new BitSet(fieldsCount);
int fieldIndex = 0;
for (final SQLField field : fields) {
final int type = field.getType().getType();
booleanFields.set(fieldIndex++, type == Types.BOOLEAN || type == Types.BIT);
}
fieldIndex = 0;
 
// remove pipes in data
tmp = tmp.replace('|', ' ');
// remove inner "
tmp = commaSepPatrn.matcher(tmp).replaceAll("|");
// remove first and last "
tmp = firstLastQuotePatrn.matcher(tmp).replaceAll("");
// remove escape character (only remove \" so we can spot \|)
tmp = backSlashPatrn.matcher(tmp).replaceAll(String.valueOf('"'));
try {
// skip fields names
int i = data.indexOf('\n') + 1;
while (i < data.length()) {
final String twoChars = i + 2 <= data.length() ? data.substring(i, i + 2) : null;
if ("\\N".equals(twoChars)) {
i += 2;
} else if ("\"\"".equals(twoChars)) {
writer.write("\0");
i += 2;
} else {
final Tuple2<String, Integer> unDoubleQuote = StringUtils.unDoubleQuote(data, i);
String unquoted = unDoubleQuote.get0();
if (booleanFields.get(fieldIndex)) {
if (unquoted.equalsIgnoreCase("false")) {
unquoted = "0";
} else if (unquoted.equalsIgnoreCase("true")) {
unquoted = "1";
}
}
writer.write(unquoted);
i = unDoubleQuote.get1();
}
fieldIndex++;
if (i < data.length()) {
final char c = data.charAt(i);
if (c == ',') {
writer.write(FIELD_DELIM);
i++;
} else if (c == '\n') {
writer.write(ROW_DELIM);
i++;
if (fieldIndex != fieldsCount)
throw new IOException("Expected " + fieldsCount + " fields but got : " + fieldIndex);
fieldIndex = 0;
} else {
throw new IOException("Unexpected character after field : " + c);
}
}
}
if (fieldIndex != 0 && fieldIndex != fieldsCount)
throw new IOException("Expected " + fieldsCount + " fields but got : " + fieldIndex);
} finally {
writer.close();
}
 
// for pg types
if (true) {
tmp = boolTPatrn.matcher(tmp).replaceAll("1|");
tmp = boolFPatrn.matcher(tmp).replaceAll("0|");
tmp = boolTEndPatrn.matcher(tmp).replaceAll("|1");
tmp = boolFEndPatrn.matcher(tmp).replaceAll("|0");
execute_bcp(t, false, temp);
temp.delete();
 
tmp = dateWithOffsetPatrn.matcher(tmp).replaceAll("$1");
// MAYBE when on localhost, remove the bcp requirement (OTOH bcp should already be
// installed, just perhaps not in the path)
// checkServerLocalhost(t);
// "bulk insert " + t.getSQL() + " from " + b.quoteString(temp.getAbsolutePath()) +
// " with ( DATAFILETYPE='widechar', FIELDTERMINATOR = " + b.quoteString(FIELD_DELIM)
// + ", ROWTERMINATOR= " + b.quoteString(ROW_DELIM) +
// ", FIRSTROW=1, KEEPIDENTITY, KEEPNULLS ) ;"
}
 
// we can't specify \n as ROWTERMINATOR ms automatically prepends \r
// http://msdn.microsoft.com/en-us/library/ms191485.aspx
if (t.isRowable() && t.getOrderedFields().get(0) != t.getKey())
throw new IllegalArgumentException("MS needs ID first for " + t + " " + t.getOrderedFields());
String winNL;
if (t.isRowable()) {
winNL = newlineAndIDPatrn.matcher(tmp).replaceAll("\r\n");
// newlineAndIDPatrn doesn't match the last newline
winNL = winNL.substring(0, winNL.length() - 1) + "\r\n";
} else {
winNL = newlinePatrn.matcher(tmp).replaceAll("\r\n");
private static final String FIELD_DELIM = "<|!!|>";
private static final String ROW_DELIM = "...#~\n~#...";
 
protected void execute_bcp(final SQLTable t, final boolean dump, final File f) throws IOException {
final ProcessBuilder pb = new ProcessBuilder("bcp");
pb.command().add(t.getSQLName().quote());
pb.command().add(dump ? "out" : "in");
pb.command().add(f.getAbsolutePath());
// UTF-16LE with a BOM
pb.command().add("-w");
pb.command().add("-t" + FIELD_DELIM);
pb.command().add("-r" + ROW_DELIM);
// needed if table name is a keyword (e.g. RIGHT)
pb.command().add("-q");
pb.command().add("-S" + t.getServer().getName());
pb.command().add("-U" + t.getDBSystemRoot().getDataSource().getUsername());
pb.command().add("-P" + t.getDBSystemRoot().getDataSource().getPassword());
if (!dump) {
// retain null
pb.command().add("-k");
// keep identity
pb.command().add("-E");
}
 
if (t.getName().equals("RIGHT"))
System.err.println("SQLSyntaxMS._loadData()\n\n" + tmp);
 
final File temp = File.createTempFile("mssql_loadData", ".txt", new File("."));
FileUtils.write(winNL, temp, "UTF-16", false);
checkServerLocalhost(t);
t.getDBSystemRoot().getDataSource()
.execute(t.getBase().quote("bulk insert %f from %s with ( DATAFILETYPE='widechar', FIELDTERMINATOR = '|', FIRSTROW=1, KEEPIDENTITY ) ;", t, temp.getAbsolutePath()));
temp.delete();
final Process p = pb.start();
ProcessStreams.handle(p, Action.REDIRECT);
try {
final int returnCode = p.waitFor();
if (returnCode != 0)
throw new IOException("Did not finish correctly : " + returnCode + "\n" + pb.command());
} catch (InterruptedException e) {
throw new RTInterruptedException(e);
}
}
 
// FIXME
// For bcp : http://www.microsoft.com/en-us/download/details.aspx?id=16978
@Override
protected void _storeData(final SQLTable t, final File f) {
checkServerLocalhost(t);
protected void _storeData(final SQLTable t, final File f) throws IOException {
final File tmpFile = File.createTempFile(FileUtils.sanitize("mssql_dump_" + t.getName()), ".dat");
execute_bcp(t, true, tmpFile);
final int readerBufferSize = 32768;
final BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(tmpFile), StringUtils.UTF16), readerBufferSize);
final List<SQLField> orderedFields = t.getOrderedFields();
final int fieldsCount = orderedFields.size();
final String cols = CollectionUtils.join(orderedFields, ",", new ITransformer<SQLField, String>() {
@Override
public String transformChecked(SQLField input) {
return SQLBase.quoteIdentifier(input.getName());
}
});
final FileOutputStream outs = new FileOutputStream(f);
BufferedWriter writer = null;
try {
writer = new BufferedWriter(new OutputStreamWriter(outs, StringUtils.UTF8));
writer.write(cols);
writer.write('\n');
final StringBuilder sb = new StringBuilder(readerBufferSize * 2);
String row = readUntil(reader, sb, ROW_DELIM);
final Pattern fieldPattern = Pattern.compile(FIELD_DELIM, Pattern.LITERAL);
while (row != null) {
if (row.length() > 0) {
// -1 to have every (even empty) field
final String[] fields = fieldPattern.split(row, -1);
if (fields.length != fieldsCount)
throw new IOException("Invalid fields count, expected " + fieldsCount + " but was " + fields.length + "\n" + row);
int i = 0;
for (final String field : fields) {
final String quoted;
if (field.length() == 0) {
quoted = "\\N";
} else if (field.equals("\0")) {
quoted = "\"\"";
} else {
quoted = StringUtils.doubleQuote(field);
}
writer.write(quoted);
if (++i < fieldsCount)
writer.write(',');
}
writer.write('\n');
}
row = readUntil(reader, sb, ROW_DELIM);
}
} finally {
tmpFile.delete();
if (writer != null)
writer.close();
else
outs.close();
reader.close();
}
}
 
private String readUntil(BufferedReader reader, StringBuilder sb, String rowDelim) throws IOException {
if (sb.capacity() == 0)
return null;
final int existing = sb.indexOf(rowDelim);
if (existing >= 0) {
final String res = sb.substring(0, existing);
sb.delete(0, existing + rowDelim.length());
return res;
} else {
final char[] buffer = new char[sb.capacity() / 3];
final int readCount = reader.read(buffer);
if (readCount <= 0) {
final String res = sb.toString();
sb.setLength(0);
sb.trimToSize();
assert sb.capacity() == 0;
return res;
} else {
sb.append(buffer, 0, readCount);
return readUntil(reader, sb, rowDelim);
}
}
}
 
@Override
public boolean supportMultiAlterClause() {
// support multiple if you omit the "add" : ALTER TABLE t add f1 int, f2 bit
294,14 → 497,16
 
@Override
public String getColumnsQuery(SQLBase b, TablesMap tables) {
// TODO
return null;
return "SELECT TABLE_SCHEMA as \"" + INFO_SCHEMA_NAMES_KEYS.get(0) + "\", TABLE_NAME as \"" + INFO_SCHEMA_NAMES_KEYS.get(1) + "\", COLUMN_NAME as \"" + INFO_SCHEMA_NAMES_KEYS.get(2)
+ "\" , CHARACTER_SET_NAME as \"CHARACTER_SET_NAME\", COLLATION_NAME as \"COLLATION_NAME\" from INFORMATION_SCHEMA.COLUMNS\n" +
// requested tables
getTablesMapJoin(b, tables, "TABLE_SCHEMA", "TABLE_NAME");
}
 
@Override
public List<Map<String, Object>> getConstraints(SQLBase b, TablesMap tables) throws SQLException {
final String where = getTablesMapJoin(b, tables, "SCHEMA_NAME(t.schema_id)", "t.name");
final String sel = "SELECT SCHEMA_NAME(t.schema_id) AS \"TABLE_SCHEMA\", t.name AS \"TABLE_NAME\", k.name AS \"CONSTRAINT_NAME\", case k.type when 'UQ' then 'UNIQUE' when 'PK' then 'PRIMARY KEY' end as \"CONSTRAINT_TYPE\", col_name(c.object_id, c.column_id) AS \"COLUMN_NAME\", c.key_ordinal AS \"ORDINAL_POSITION\"\n"
final String sel = "SELECT SCHEMA_NAME(t.schema_id) AS \"TABLE_SCHEMA\", t.name AS \"TABLE_NAME\", k.name AS \"CONSTRAINT_NAME\", case k.type when 'UQ' then 'UNIQUE' when 'PK' then 'PRIMARY KEY' end as \"CONSTRAINT_TYPE\", col_name(c.object_id, c.column_id) AS \"COLUMN_NAME\", c.key_ordinal AS \"ORDINAL_POSITION\", null AS [DEFINITION]\n"
+ "FROM sys.key_constraints k\n"
//
+ "JOIN sys.index_columns c ON c.object_id = k.parent_object_id AND c.index_id = k.unique_index_id\n"
310,7 → 515,7
+ where
+ "\nUNION ALL\n"
//
+ "SELECT SCHEMA_NAME(t.schema_id) AS \"TABLE_SCHEMA\", t.name AS \"TABLE_NAME\", k.name AS \"CONSTRAINT_NAME\", 'CHECK' as \"CONSTRAINT_TYPE\", col.name AS \"COLUMN_NAME\", 1 AS \"ORDINAL_POSITION\"\n"
+ "SELECT SCHEMA_NAME(t.schema_id) AS \"TABLE_SCHEMA\", t.name AS \"TABLE_NAME\", k.name AS \"CONSTRAINT_NAME\", 'CHECK' as \"CONSTRAINT_TYPE\", col.name AS \"COLUMN_NAME\", 1 AS \"ORDINAL_POSITION\", k.[definition] AS [DEFINITION]\n"
+ "FROM sys.check_constraints k\n"
//
+ "join sys.tables t on k.parent_object_id = t.object_id\n"
317,6 → 522,16
//
+ "left join sys.columns col on k.parent_column_id = col.column_id and col.object_id = t.object_id\n"
//
+ where
+ "\nUNION ALL\n"
//
+ "SELECT SCHEMA_NAME(t.schema_id) AS [TABLE_SCHEMA], t.name AS [TABLE_NAME], k.name AS [CONSTRAINT_NAME], 'DEFAULT' as [CONSTRAINT_TYPE], col.name AS [COLUMN_NAME], 1 AS [ORDINAL_POSITION], k.[definition] AS [DEFINITION]\n"
+ "FROM sys.[default_constraints] k\n"
//
+ "JOIN sys.tables t ON t.object_id = k.parent_object_id\n"
//
+ "left join sys.columns col on k.parent_column_id = col.column_id and col.object_id = t.object_id\n"
//
+ where;
// don't cache since we don't listen on system tables
@SuppressWarnings("unchecked")
344,7 → 559,7
 
@Override
public String getFormatTimestamp(String sqlTS, boolean basic) {
final String extended = "CONVERT(nvarchar(30), " + sqlTS + ", 126) + '000'";
final String extended = "CONVERT(nvarchar(30), CAST(" + sqlTS + " as datetime), 126) + '000'";
if (basic) {
return "replace( replace( " + extended + ", '-', ''), ':' , '' )";
} else {
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLRowValues.java
26,6 → 26,7
import org.openconcerto.sql.request.Inserter.ReturnMode;
import org.openconcerto.sql.users.UserManager;
import org.openconcerto.sql.utils.ReOrder;
import org.openconcerto.utils.CollectionMap2Itf.SetMapItf;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.CopyUtils;
import org.openconcerto.utils.ExceptionUtils;
150,6 → 151,17
 
private static final boolean DEFAULT_ALLOW_BACKTRACK = true;
 
// i.e. no re-hash for up to 6 entries (8*0.8=6.4)
private static final int DEFAULT_VALUES_CAPACITY = 8;
private static final float DEFAULT_LOAD_FACTOR = 0.8f;
 
// Assure there's no copy. Don't just return plannedSize : e.g. for HashMap if it's 15
// the initial capacity will be 16 (the nearest power of 2) and threshold will be 12.8 (with
// our load of 0.8) so there would be a rehash at the 13th items.
private static final int getCapacity(final int plannedSize, final int defaultCapacity) {
return plannedSize < 0 ? defaultCapacity : Math.max((int) (plannedSize / DEFAULT_LOAD_FACTOR) + 1, 4);
}
 
private final Map<String, Object> values;
private final Map<String, SQLRowValues> foreigns;
private final SetMap<SQLField, SQLRowValues> referents;
157,12 → 169,29
private ListMap<SQLField, ReferentChangeListener> referentsListener;
 
public SQLRowValues(SQLTable t) {
this(t, -1, -1, -1);
}
 
/**
* Create a new instance.
*
* @param t the table.
* @param valuesPlannedSize no further allocations will be made until that number of
* {@link #getAbsolutelyAll() values}, pass a negative value to use a default.
* @param foreignsPlannedSize no further allocations will be made until that number of
* {@link #getForeigns() foreigns}, pass a negative value to use a default.
* @param referentsPlannedSize no further allocations will be made until that number of
* {@link #getReferentsMap() referents}, pass a negative value to use a default.
*
*/
public SQLRowValues(SQLTable t, final int valuesPlannedSize, final int foreignsPlannedSize, final int referentsPlannedSize) {
super(t);
// use LinkedHashSet so that the order is preserved, see #walkFields()
// don't use value too low for initialCapacity otherwise rehash operations
this.values = new LinkedHashMap<String, Object>(8);
this.foreigns = new HashMap<String, SQLRowValues>(4);
this.referents = new SetMap<SQLField, SQLRowValues>(4, org.openconcerto.utils.CollectionMap2.Mode.NULL_FORBIDDEN, false) {
this.values = new LinkedHashMap<String, Object>(getCapacity(valuesPlannedSize, DEFAULT_VALUES_CAPACITY), DEFAULT_LOAD_FACTOR);
// foreigns order should be coherent with values
this.foreigns = new LinkedHashMap<String, SQLRowValues>(getCapacity(foreignsPlannedSize, 4), DEFAULT_LOAD_FACTOR);
this.referents = new SetMap<SQLField, SQLRowValues>(new HashMap<SQLField, Set<SQLRowValues>>(getCapacity(referentsPlannedSize, 4), DEFAULT_LOAD_FACTOR),
org.openconcerto.utils.CollectionMap2.Mode.NULL_FORBIDDEN, false) {
@Override
public Set<SQLRowValues> createCollection(Collection<? extends SQLRowValues> coll) {
// use LinkedHashSet so that the order is preserved, eg we can iterate over LOCALs
173,11 → 202,16
};
// no used much so lazy init
this.referentsListener = null;
this.graph = new SQLRowValuesCluster(this);
// Allow to reduce memory for lonely rows, and even for linked rows since before :
// 1. create a row, create a cluster
// 2. create a second row, create a second cluster
// 3. put, the second row uses the first cluster, the second one can be collected
// Now the second cluster is never created, see SQLRowValuesCluster.add().
this.graph = null;
}
 
public SQLRowValues(SQLTable t, Map<String, ?> values) {
this(t);
this(t, values.size(), -1, -1);
this.setAll(values);
}
 
195,9 → 229,8
* @param copyForeigns whether to copy foreign SQLRowValues.
*/
public SQLRowValues(SQLRowValues vals, ForeignCopyMode copyForeigns) {
this(vals.getTable());
// setAll() takes care of foreigns and referents
this.setAll(vals.getAllValues(copyForeigns));
this(vals.getTable(), vals.getAllValues(copyForeigns));
}
 
@Override
218,7 → 251,7
 
// *** graph
 
private synchronized void updateLinks(String fieldName, Object old, Object value) {
private void updateLinks(String fieldName, Object old, Object value) {
// try to avoid getTable().getField() (which takes 1/3 of put() for nothing when there is no
// rowvalues)
final boolean oldRowVals = old instanceof SQLRowValues;
240,13 → 273,21
final SQLRowValues vals = (SQLRowValues) value;
vals.referents.add(f, this);
this.foreigns.put(fieldName, vals);
this.graph.add(this, f, vals);
// prefer vals' graph as add() is faster that way
final SQLRowValuesCluster usedGraph = this.graph != null && vals.graph == null ? this.graph : vals.getGraph();
usedGraph.add(this, f, vals);
assert this.graph == vals.graph;
vals.fireRefChange(f, true, this);
}
}
 
public synchronized final SQLRowValuesCluster getGraph() {
public final SQLRowValuesCluster getGraph() {
return this.getGraph(true);
}
 
final SQLRowValuesCluster getGraph(final boolean create) {
if (create && this.graph == null)
this.graph = new SQLRowValuesCluster(this);
return this.graph;
}
 
322,7 → 363,12
this.graph = g;
}
 
final Map<String, SQLRowValues> getForeigns() {
public final boolean hasForeigns() {
// OK since updateLinks() removes empty map entries
return !this.foreigns.isEmpty();
}
 
public final Map<String, SQLRowValues> getForeigns() {
return Collections.unmodifiableMap(this.foreigns);
}
 
344,6 → 390,15
return this.referents;
}
 
public final SetMapItf<SQLField, SQLRowValues> getReferentsMap() {
return SetMap.unmodifiableMap(this.referents);
}
 
public final boolean hasReferents() {
// OK since updateLinks() removes empty map entries
return !this.referents.isEmpty();
}
 
@Override
public Collection<SQLRowValues> getReferentRows() {
// remove the backdoor since values() returns a view
370,7 → 425,7
}
 
/**
* Remove all links pointing to this.
* Remove all links pointing to this from the referent rows.
*
* @return this.
*/
396,7 → 451,7
for (final Entry<SQLField, Set<SQLRowValues>> e : CopyUtils.copy(this.getReferents()).entrySet()) {
if (f == null || e.getKey().equals(f) != retain) {
for (final SQLRowValues ref : e.getValue()) {
ref.put(e.getKey().getName(), null);
ref.remove(e.getKey().getName());
}
}
}
414,7 → 469,7
for (final Entry<SQLField, Set<SQLRowValues>> e : CopyUtils.copy(this.getReferents()).entrySet()) {
for (final SQLRowValues ref : e.getValue()) {
if (!toRetain.contains(ref))
ref.put(e.getKey().getName(), null);
ref.remove(e.getKey().getName());
}
}
return this;
428,7 → 483,7
 
@Override
public final int getID() {
final Number res = this.getIDNumber();
final Number res = this.getIDNumber(false);
if (res != null)
return res.intValue();
else
436,16 → 491,33
}
 
@Override
public final Number getIDNumber() {
final Object res = this.getObject(this.getTable().getKey().getName());
if (res instanceof Number) {
public Number getIDNumber() {
// We never have rows in the DB with NULL primary key, so a null result means no value was
// specified (or null was programmatically specified)
return this.getIDNumber(false);
}
 
public final Number getIDNumber(final boolean mustBePresent) {
final Object res = this.getObject(this.getTable().getKey().getName(), mustBePresent);
if (res == null) {
return null;
} else {
return (Number) res;
} else
return null;
}
}
 
@Override
public final Object getObject(String fieldName) {
return this.getObject(fieldName, false);
}
 
private Object getContainedObject(String fieldName) throws IllegalArgumentException {
return this.getObject(fieldName, true);
}
 
private Object getObject(String fieldName, final boolean mustBePresent) throws IllegalArgumentException {
if (mustBePresent && !this.values.containsKey(fieldName))
throw new IllegalArgumentException("Field " + fieldName + " not present in this : " + this.getFields());
return this.values.get(fieldName);
}
 
506,41 → 578,43
}
}
 
private Object getContainedObject(String fieldName) throws IllegalArgumentException {
if (!this.values.containsKey(fieldName))
throw new IllegalArgumentException("Field " + fieldName + " not present in this : " + this.getFields());
return this.values.get(fieldName);
}
 
/**
* Returns the foreign table of <i>fieldName</i>.
*
* @param fieldName the name of a foreign field, eg "ID_ARTICLE_2".
* @return the table the field points to (never <code>null</code>), eg |ARTICLE|.
* @param fieldName the name of a foreign field, e.g. "ID_ARTICLE_2".
* @return the table the field points to (never <code>null</code>), e.g. |ARTICLE|.
* @throws IllegalArgumentException if <i>fieldName</i> is not a foreign field.
*/
private final SQLTable getForeignTable(String fieldName) throws IllegalArgumentException {
return this.getForeignTable(Collections.singletonList(fieldName));
}
 
private final SQLTable getForeignTable(final List<String> fieldsNames) throws IllegalArgumentException {
final DatabaseGraph graph = this.getTable().getDBSystemRoot().getGraph();
final SQLTable foreignTable = graph.getForeignTable(this.getTable().getField(fieldName));
if (foreignTable == null)
throw new IllegalArgumentException(fieldName + " is not a foreign key of " + this.getTable());
return foreignTable;
final Link foreignLink = graph.getForeignLink(this.getTable(), fieldsNames);
if (foreignLink == null)
throw new IllegalArgumentException(fieldsNames + " are not a foreign key of " + this.getTable());
return foreignLink.getTarget();
}
 
@Override
public boolean isForeignEmpty(String fieldName) {
// don't use getForeign() to avoid creating a SQLRow
// keep getForeignTable at the 1st line since it does the check
final SQLTable foreignTable = this.getForeignTable(fieldName);
final Object val = this.getContainedObject(fieldName);
final Number id = val instanceof SQLRowValues ? ((SQLRowValues) val).getIDNumber() : (Number) val;
if (val instanceof SQLRowValues) {
return ((SQLRowValues) val).isUndefined();
} else {
final Number undefID = foreignTable.getUndefinedIDNumber();
return NumberUtils.areNumericallyEqual(id, undefID);
return NumberUtils.areNumericallyEqual((Number) val, undefID);
}
}
 
@Override
public int getForeignID(String fieldName) {
public Number getForeignIDNumber(String fieldName) throws IllegalArgumentException {
final SQLRowAccessor foreign = getForeign(fieldName);
return foreign == null ? SQLRow.NONEXISTANT_ID : foreign.getID();
return foreign == null ? null : foreign.getIDNumber();
}
 
public boolean isDefault(String fieldName) {
557,15 → 631,6
return Collections.unmodifiableSet(this.values.keySet());
}
 
/**
* Whether this row has a Number for the primary key.
*
* @return <code>true</code> if the value of the primary key is a number.
*/
public final boolean hasID() {
return this.getIDNumber() != null;
}
 
@Override
public final SQLRow asRow() {
if (!this.hasID())
623,7 → 688,10
} else {
this.values.keySet().removeAll(toRm);
}
this.getGraph().fireModification(this, toRm);
// if there's no graph, there can't be any listeners
final SQLRowValuesCluster graph = this.getGraph(false);
if (graph != null)
graph.fireModification(this, toRm);
return this;
}
 
682,7 → 750,10
if (checkName && !this.getTable().contains(fieldName))
throw new IllegalArgumentException(fieldName + " is not in table " + this.getTable());
_put(fieldName, value);
this.getGraph().fireModification(this, fieldName, value);
// if there's no graph, there can't be any listeners
final SQLRowValuesCluster graph = this.getGraph(false);
if (graph != null)
graph.fireModification(this, fieldName, value);
return this;
}
 
756,6 → 827,42
}
 
/**
* Safely set the passed field to the value of the primary key of <code>r</code>.
*
* @param fk the field to change.
* @param r the row, <code>null</code> meaning {@link #SQL_EMPTY_LINK empty} foreign key.
* @return this.
* @throws IllegalArgumentException if <code>fk</code> doesn't point to the table of
* <code>r</code>.
*/
public final SQLRowValues putForeignID(final String fk, final SQLRowAccessor r) throws IllegalArgumentException {
return this.putForeignKey(Collections.singletonList(fk), r);
}
 
public final SQLRowValues putForeignKey(final List<String> cols, final SQLRowAccessor r) throws IllegalArgumentException {
// first check that cols are indeed a foreign key
final SQLTable foreignTable = this.getForeignTable(cols);
if (r == null) {
if (cols.size() == 1) {
return this.putEmptyLink(cols.get(0));
} else {
return this.loadAll(CollectionUtils.fillMap(new HashMap<String, Object>(cols.size()), cols, SQL_EMPTY_LINK), false);
}
} else {
checkSameTable(r, foreignTable);
if (cols.size() == 1)
return this.put(cols.get(0), r.getIDNumber());
else
return this.loadAll(r.getAbsolutelyAll(), cols, true, false);
}
}
 
private void checkSameTable(final SQLRowAccessor r, final SQLTable t) {
if (r.getTable() != t)
throw new IllegalArgumentException("Table mismatch : " + r.getTable().getSQLName() + " != " + t.getSQLName());
}
 
/**
* Set the order of this row so that it will be just after/before <code>r</code>. NOTE: this may
* reorder the table to make room.
*
809,6 → 916,16
return this.put(key.getName(), id);
}
 
public final SQLRowValues setPrimaryKey(final SQLRowAccessor r) {
if (r == null) {
return this.putNulls(this.getTable().getPKsNames(), false);
} else {
checkSameTable(r, this.getTable());
// required since we don't want only half of the fields of the primary key
return this.loadAll(r.getAbsolutelyAll(), this.getTable().getPKsNames(new HashSet<String>()), true, false);
}
}
 
public final SQLRowValues setAll(Map<String, ?> m) {
return this.loadAll(m, true);
}
818,21 → 935,40
}
 
private final SQLRowValues loadAll(Map<String, ?> m, final boolean clear) {
if (!this.getTable().getFieldsName().containsAll(m.keySet()))
throw new IllegalArgumentException("fields " + m.keySet() + " are not a subset of " + this.getTable() + " : " + this.getTable().getFieldsName());
return this.loadAll(m, null, false, clear);
}
 
private final SQLRowValues loadAll(Map<String, ?> m, final Collection<String> keys, final boolean required, final boolean clear) {
final Collection<String> keySet = keys == null ? m.keySet() : keys;
if (!this.getTable().getFieldsName().containsAll(keySet))
throw new IllegalArgumentException("fields " + keySet + " are not a subset of " + this.getTable() + " : " + this.getTable().getFieldsName());
// copy before passing to fire()
final Map<String, Object> toLoad = new HashMap<String, Object>(m);
if (keys != null) {
if (required && !m.keySet().containsAll(keys))
throw new IllegalArgumentException("Not all are keys " + keys + " are in " + m);
toLoad.keySet().retainAll(keys);
}
if (clear)
clear();
for (final Map.Entry<String, ?> e : m.entrySet()) {
for (final Map.Entry<String, ?> e : toLoad.entrySet()) {
this._put(e.getKey(), e.getValue());
}
this.getGraph().fireModification(this, m);
// if there's no graph, there can't be any listeners
final SQLRowValuesCluster graph = this.getGraph(false);
if (graph != null)
graph.fireModification(this, toLoad);
return this;
}
 
public final SQLRowValues putNulls(String... fields) {
return this.putNulls(Arrays.asList(fields), false);
return this.putNulls(Arrays.asList(fields));
}
 
public final SQLRowValues putNulls(Collection<String> fields) {
return this.putNulls(fields, false);
}
 
/**
* Set the passed fields to <code>null</code>.
*
947,6 → 1083,8
for (final ReferentChangeListener l : this.referentsListener.getNonNull(null))
l.referentChange(evt);
}
// no need to avoid creating graph, as this is called when the graph change
assert this.graph != null;
this.getGraph().fireModification(evt);
}
}
1190,7 → 1328,7
* <li>en 1 une SQLRow décrivant le pb, eg "(OBSERVATION[123])"</li>
* </ol>
*/
public synchronized Object[] getInvalid() {
public Object[] getInvalid() {
final Set<SQLField> fk = this.getTable().getForeignKeys();
for (final String fieldName : this.values.keySet()) {
final SQLField field = this.getTable().getField(fieldName);
1217,7 → 1355,7
* @throws SQLException if an error occurs while inserting.
* @throws IllegalStateException if the ID of the new line cannot be retrieved.
*/
public synchronized SQLRow insert() throws SQLException {
public SQLRow insert() throws SQLException {
// remove unwanted fields, keep ARCHIVE
return this.insert(false, false);
}
1230,13 → 1368,12
* @throws SQLException if an error occurs while inserting.
* @throws IllegalStateException if the ID of the new line cannot be retrieved.
*/
public synchronized SQLRow insertVerbatim() throws SQLException {
public SQLRow insertVerbatim() throws SQLException {
return this.insert(true, true);
}
 
public synchronized SQLRow insert(final boolean insertPK, final boolean insertOrder) throws SQLException {
this.getGraph().store(new SQLRowValuesCluster.Insert(insertPK, insertOrder));
return this.getGraph().getRow(this);
public SQLRow insert(final boolean insertPK, final boolean insertOrder) throws SQLException {
return this.getGraph().store(new SQLRowValuesCluster.Insert(insertPK, insertOrder)).getStoredRow(this);
}
 
SQLTableEvent insertJustThis(final Set<SQLField> autoFields) throws SQLException {
1280,9 → 1417,9
 
public SQLRow update() throws SQLException {
if (!hasID()) {
throw new IllegalStateException("can't update no ID specified, use update(int)");
throw new IllegalStateException("can't update : no ID specified, use update(int) or set ID for " + this);
}
return this.update(this.getID());
return this.commit();
}
 
public SQLRow update(final int id) throws SQLException {
1299,7 → 1436,7
*/
SQLTableEvent updateJustThis(final int id) throws SQLException {
if (id == this.getTable().getUndefinedID()) {
throw new IllegalArgumentException("can't update undefined");
throw new IllegalArgumentException("can't update undefined with " + this);
}
// clear primary key, otherwise we might end up with :
// UPDATE TABLE SET ID=123,DESIGNATION='aa' WHERE id=456
1306,7 → 1443,11
// which will delete ID 456, and possibly cause a conflict with preexisting ID 123
final Map<String, Object> updatedValues = this.clearPrimaryKeys(new HashMap<String, Object>(this.values));
 
final List<String> updatedCols = this.getTable().getDBSystemRoot().getDataSource().useConnection(new ConnectionHandlerNoSetup<List<String>, SQLException>() {
final List<String> updatedCols;
if (updatedValues.isEmpty()) {
updatedCols = Collections.emptyList();
} else {
updatedCols = this.getTable().getDBSystemRoot().getDataSource().useConnection(new ConnectionHandlerNoSetup<List<String>, SQLException>() {
@Override
public List<String> handle(SQLDataSource ds) throws SQLException {
final Tuple2<PreparedStatement, List<String>> pStmt = createUpdateStatement(getTable(), updatedValues, id);
1320,6 → 1461,7
return pStmt.get1();
}
});
}
 
return new SQLTableEvent(getChangedRow(id), Mode.ROW_UPDATED, updatedCols);
}
1334,8 → 1476,7
* @throws SQLException
*/
public SQLRow commit() throws SQLException {
this.getGraph().store(SQLRowValuesCluster.StoreMode.COMMIT);
return this.getGraph().getRow(this);
return this.getGraph().store(SQLRowValuesCluster.StoreMode.COMMIT).getStoredRow(this);
}
 
SQLTableEvent commitJustThis() throws SQLException {
1430,9 → 1571,22
* @return the first difference, <code>null</code> if equals.
*/
public final String getGraphFirstDifference(final SQLRowValues other) {
return this.getGraph().getFirstDifference(this, other);
return this.getGraphFirstDifference(other, false);
}
 
/**
* Return the first difference between this graph and another. Most of the time foreigns order
* need not to be used, since when inserting they don't matter (which isn't true of the
* referents). But they can matter if e.g. this is used to construct a query.
*
* @param other another instance.
* @param useForeignsOrder <code>true</code> to also compare foreigns order.
* @return the first difference, <code>null</code> if equals.
*/
public final String getGraphFirstDifference(final SQLRowValues other, final boolean useForeignsOrder) {
return this.getGraph().getFirstDifference(this, other, useForeignsOrder);
}
 
final boolean equalsJustThis(final SQLRowValues o) {
// NO_COPY since foreign rows are handled by SQLRowValuesCluster.equals()
// LinkedHashMap.equals() does not compare the order of entries, which is fine since
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLDataSource.java
228,6 → 228,22
// instead of List<String>) and faster (less trips to the server, allow
// SQLUtils.executeMultiple())
this.addConnectionProperty("allowMultiQueries", "true");
} else if (system == SQLSystem.MSSQL) {
// Otherwise we get SQLState S0002 instead of 42S02 (needed at least in
// SQLBase.getFwkMetadata())
// http://msdn.microsoft.com/fr-fr/library/ms712451.aspx
// http://technet.microsoft.com/en-us/library/ms378988(v=sql.105).aspx
this.addConnectionProperty("xopenStates", "true");
// see
// https://connect.microsoft.com/SQLServer/feedback/details/295907/resultsetmetadata-gettablename-returns-null-or-inconsistent-results
// http://social.msdn.microsoft.com/Forums/sqlserver/en-US/55e8cbb2-b11c-446e-93ab-dc30658caf99/resultsetmetadatagettablename-returns-instead-of-table-name?forum=sqldataaccess
// 1. The statement that the resultset belongs to was created with
// TYPE_SCROLL_INSENSITIVE or TYPE_SCROLL_SENSITIVE : The full table or view name will
// be returned
// 2. The statement that the resultset belongs to was created without specifying the
// cursor type, or the cursor type is TYPE_FORWARD_ONLY : The full table or view name
// will be returned if the column is a text, ntext, or image, else empty string.
this.addConnectionProperty("selectMethod", "cursor");
}
this.setLoginTimeout(loginTimeOut);
this.setSocketTimeout(socketTimeOut);
241,7 → 257,7
public final void setLoginTimeout(int timeout) {
if (this.getSystem() == SQLSystem.MYSQL) {
this.addConnectionProperty("connectTimeout", timeout + "000");
} else if (this.getSystem() == SQLSystem.POSTGRESQL) {
} else if (this.getSystem() == SQLSystem.POSTGRESQL || this.getSystem() == SQLSystem.MSSQL) {
this.addConnectionProperty("loginTimeout", timeout + "");
}
}
925,7 → 941,8
// MAYBE un truc un peu plus formel
if (query.startsWith("INSERT") || query.startsWith("UPDATE") || query.startsWith("DELETE") || query.startsWith("CREATE") || query.startsWith("ALTER") || query.startsWith("DROP")
|| query.startsWith("SET")) {
final boolean returnGenK = (query.startsWith("INSERT") || query.startsWith("UPDATE")) && stmt.getConnection().getMetaData().supportsGetGeneratedKeys();
// MS SQL doesn't support UPDATE
final boolean returnGenK = query.startsWith("INSERT") && stmt.getConnection().getMetaData().supportsGetGeneratedKeys();
stmt.executeUpdate(query, returnGenK ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS);
rs = returnGenK ? stmt.getGeneratedKeys() : null;
} else {
1337,7 → 1354,8
// don't call setSavepoint() if no stack
final HandlersStack handlersStack = getNonNullHandlersStack();
final Savepoint res = super.setSavepoint();
handlersStack.addTxPoint(new TransactionPoint(this, res, false));
// MySQL always create named save points
handlersStack.addTxPoint(new TransactionPoint(this, res, getSystem() == SQLSystem.MYSQL));
return res;
}
 
1626,11 → 1644,13
q = "set session search_path to " + SQLBase.quoteIdentifier(schemaName);
}
} else if (this.getSystem() == SQLSystem.MSSQL) {
if (schemaName == null)
if (schemaName == null) {
throw new IllegalArgumentException("cannot unset default schema in " + this.getSystem());
else
q = "alter user " + getUsername() + " with default_schema = " + SQLBase.quoteIdentifier(schemaName);
} else {
// ATTN MSSQL apparently hang until the connection that created the schema commits
q = "ALTER USER " + SQLBase.quoteIdentifier(getUsername()) + " with default_schema = " + SQLBase.quoteIdentifier(schemaName);
}
} else {
throw new UnsupportedOperationException();
}
 
1664,7 → 1684,7
return this.getUrl();
}
 
private final SQLSystem getSystem() {
public final SQLSystem getSystem() {
return this.sysRoot.getServer().getSQLSystem();
}
 
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLSchema.java
41,7 → 41,7
import net.jcip.annotations.GuardedBy;
 
import org.apache.commons.dbutils.ResultSetHandler;
import org.jdom.Element;
import org.jdom2.Element;
 
public final class SQLSchema extends SQLIdentifier {
 
87,13 → 87,13
return schemaElem.getAttributeValue(VERSION_XMLATTR);
}
 
public static final String getVersion(final SQLBase base, final String schemaName) {
public static final Map<String, String> getVersions(final SQLBase base, final Set<String> schemaNames) {
// since we haven't an instance of SQLSchema, we can't know if the table exists
return base.getFwkMetadata(schemaName, VERSION_MDKEY, true);
return base.getFwkMetadata(schemaNames, VERSION_MDKEY);
}
 
static private String getVersionSQL(final SQLSyntax syntax) {
return syntax.getFormatTimestamp("now()", true);
return syntax.getFormatTimestamp("CURRENT_TIMESTAMP", true);
}
 
static SQLCreateMoveableTable getCreateMetadata(final SQLSyntax syntax) throws SQLException {
135,6 → 135,12
return (SQLBase) this.getParent();
}
 
@Override
protected void onDrop() {
SQLTable.removeUndefID(this);
super.onDrop();
}
 
/**
* The version when this instance was last fully refreshed. In other words, if we refresh tables
* by names (even if we name them all) this version isn't updated.
173,14 → 179,11
// XMLStructureSource always pre-verify so we don't need the system root lock
void load(Element schemaElem, Set<String> tableNames) {
this.setFullyRefreshedVersion(getVersion(schemaElem));
final List<?> l = schemaElem.getChildren("table");
for (int i = 0; i < l.size(); i++) {
final Element elementTable = (Element) l.get(i);
for (final Element elementTable : schemaElem.getChildren("table")) {
this.refreshTable(elementTable, tableNames);
}
final Map<String, String> procMap = new HashMap<String, String>();
for (final Object proc : schemaElem.getChild("procedures").getChildren("proc")) {
final Element procElem = (Element) proc;
for (final Element procElem : schemaElem.getChild("procedures").getChildren("proc")) {
final Element src = procElem.getChild("src");
procMap.put(procElem.getAttributeValue("name"), src == null ? null : src.getText());
}
196,12 → 199,10
*/
final SQLTable fetchTable(final String tableName) throws SQLException {
synchronized (getTreeMutex()) {
synchronized (this) {
this.getBase().fetchTables(TablesMap.createFromTables(getName(), Collections.singleton(tableName)));
return this.getTable(tableName);
}
}
}
 
void mutateTo(SQLSchema newSchema) {
assert Thread.holdsLock(this.getDBSystemRoot().getTreeMutex());
357,7 → 358,7
return null;
 
// we just tested for table existence
return this.getBase().getFwkMetadata(this.getName(), name, false);
return this.getBase().getFwkMetadata(this.getName(), name);
}
 
boolean setFwkMetadata(String name, String value) throws SQLException {
/trunk/OpenConcerto/src/org/openconcerto/sql/model/graph/DatabaseGraph.java
38,9 → 38,10
import org.openconcerto.sql.model.graph.Link.Direction;
import org.openconcerto.sql.model.graph.Link.Rule;
import org.openconcerto.sql.model.graph.ToRefreshSpec.ToRefreshActual;
import org.openconcerto.utils.CollectionMap;
import org.openconcerto.utils.ArrayComparator;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.FileUtils;
import org.openconcerto.utils.SetMap;
import org.openconcerto.utils.cc.IPredicate;
 
import java.io.BufferedWriter;
55,6 → 56,7
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
69,10 → 71,10
import net.jcip.annotations.ThreadSafe;
 
import org.apache.commons.collections.CollectionUtils;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.jgrapht.Graphs;
import org.jgrapht.graph.DirectedMultigraph;
 
96,6 → 98,21
private static final String XML_VERSION = "20121024-1614";
private static final String FILENAME = "graph.xml";
 
// Some systems follow the JDBC to the letter and order by PKTABLE_CAT, PKTABLE_SCHEM,
// PKTABLE_NAME, KEY_SEQ : thus ignoring FK_NAME
static final Comparator<Object[]> IMPORTED_KEYS_COMP;
 
static {
// PKTABLE_CAT, PKTABLE_SCHEM, PKTABLE_NAME, FK_NAME, KEY_SEQ
final List<Comparator<Object[]>> comps = new ArrayList<Comparator<Object[]>>();
comps.add(ArrayComparator.createNatural(0, String.class));
comps.add(ArrayComparator.createNatural(1, String.class));
comps.add(ArrayComparator.createNatural(2, String.class));
comps.add(ArrayComparator.createNatural(11, String.class));
comps.add(ArrayComparator.createNatural(8, Short.class));
IMPORTED_KEYS_COMP = CompareUtils.createComparator(comps);
}
 
// passedBase is the base that was passed for the catalog parameter of getImportedKeys() or
// getExportedKeys()
public static SQLTable getTableFromJDBCMetaData(final SQLBase passedBase, final String jdbcCat, final String jdbcSchem, final String jdbcName) {
118,6 → 135,7
if (server.getSQLSystem() == SQLSystem.MYSQL)
// MySQL returns all lowercase foreignTableName, see Bug #18446 :
// INFORMATION_SCHEMA.KEY_COLUMN_USAGE.REFERENCED_TABLE_NAME always lowercase
// also http://bugs.mysql.com/bug.php?id=60773
res = getTableIgnoringCase(schema, jdbcName);
else
res = (SQLTable) schema.getCheckedChild(jdbcName);
378,12 → 396,19
}
}
 
private Rule getRule(final Number n, final SQLSystem sys) {
final Rule res = Rule.fromShort(n.shortValue());
// MS SQL incorrectly report RESTRICT as it doesn't support it
return sys == SQLSystem.MSSQL && Rule.RESTRICT.equals(res) ? Rule.NO_ACTION : res;
}
 
private void map(final DBRoot r, final String tableName, final Set<String> tableNames) throws SQLException {
// either we refresh the whole root and we must know which tables to use
// or we refresh only one table and tableNames is useless
assert tableName == null ^ tableNames == null;
final CollectionMap<String, String> metadataFKs = new CollectionMap<String, String>(new HashSet<String>());
final List importedKeys = this.base.getDataSource().useConnection(new ConnectionHandlerNoSetup<List, SQLException>() {
final SetMap<String, String> metadataFKs = new SetMap<String, String>();
@SuppressWarnings("unchecked")
final List<Object[]> importedKeys = this.base.getDataSource().useConnection(new ConnectionHandlerNoSetup<List, SQLException>() {
@Override
public List handle(final SQLDataSource ds) throws SQLException {
final DatabaseMetaData metaData = ds.getConnection().getMetaData();
393,12 → 418,18
// accumulators for multi-field foreign key
final List<SQLField> from = new ArrayList<SQLField>();
final List<SQLField> to = new ArrayList<SQLField>();
final SQLSystem sys = this.base.getServer().getSQLSystem();
Rule updateRule = null;
Rule deleteRule = null;
String name = null;
final Iterator ikIter = importedKeys.iterator();
// Follow the JDBC to the letter and order by PKTABLE_CAT, PKTABLE_SCHEM, PKTABLE_NAME,
// KEY_SEQ : thus ignoring FK_NAME
if (sys == SQLSystem.MSSQL) {
Collections.sort(importedKeys, IMPORTED_KEYS_COMP);
}
final Iterator<Object[]> ikIter = importedKeys.iterator();
while (ikIter.hasNext()) {
final Object[] m = (Object[]) ikIter.next();
final Object[] m = ikIter.next();
 
// FKTABLE_SCHEM
assert CompareUtils.equals(m[5], r.getSchema().getName());
426,7 → 457,7
throw new IllegalStateException("Could not find what " + key.getSQLName() + " references", e);
}
 
metadataFKs.put(fkTableName, keyName);
metadataFKs.add(fkTableName, keyName);
if (seq == 1) {
// if we start a new link add the current one
if (from.size() > 0)
442,9 → 473,9
final Rule prevUpdateRule = updateRule;
final Rule prevDeleteRule = deleteRule;
// "UPDATE_RULE"
updateRule = Rule.fromShort(((Number) m[9]).shortValue());
updateRule = getRule((Number) m[9], sys);
// "DELETE_RULE"
deleteRule = Rule.fromShort(((Number) m[10]).shortValue());
deleteRule = getRule((Number) m[10], sys);
if (seq > 1) {
if (prevUpdateRule != updateRule)
throw new IllegalStateException("Incoherent update rules " + prevUpdateRule + " != " + updateRule);
612,12 → 643,10
if (!CompareUtils.equals(xmlVersion, actualVersion))
throw new IOException("wrong DB version, expected " + actualVersion + " got: " + xmlVersion);
final Set<String> fromXMLTableNames = fromXML.get(rootName);
for (final Object o : doc.getRootElement().getChildren()) {
final Element tableElem = (Element) o;
for (final Element tableElem : doc.getRootElement().getChildren()) {
final SQLTable t = r.getTable(tableElem.getAttributeValue("name"));
if (fromXMLTableNames.contains(t.getName())) {
for (final Object lo : tableElem.getChildren()) {
final Element linkElem = (Element) lo;
for (final Element linkElem : tableElem.getChildren()) {
addLink(Link.fromXML(t, linkElem));
}
// t was loaded (even if it had no links)
702,12 → 731,12
public Link getLink(final SQLTable table, final Direction dir, final IPredicate<? super Link> pred, final boolean nullIfNone) {
final Set<Link> res = this.getLinks(table, dir, pred);
if (res.size() > 1) {
throw new IllegalStateException("More than one link : " + res);
throw new IllegalStateException("More than one link from " + table + " with direction " + dir + " and predicate " + pred + " : " + res);
} else if (res.size() == 0) {
if (nullIfNone)
return null;
else
throw new IllegalStateException("No link");
throw new IllegalStateException("No link from " + table + " with direction " + dir + " and predicate " + pred);
}
return res.iterator().next();
}
900,7 → 929,7
* Renvoie la clause WHERE pour faire la jointure en t1 et t2. Par exemple entre MISSION et
* RAPPORT : <br/>
* RAPPORT.ID_MISSION=MISSION.ID_MISSION OR MISSION.ID_RAPPORT_INITIAL=RAPPORT.ID_RAPPORT. Pour
* un sous-ensemble des liens, utiliser {@link #getWhereClause(SQLTable, SQLTable, Set)}, pour
* un sous-ensemble des liens, utiliser {@link #getWhereClause(TableRef, TableRef, Step)}, pour
* un seul champ {@link #getWhereClause(SQLField)}.
*
* @param t1 la premiere table.
/trunk/OpenConcerto/src/org/openconcerto/sql/model/graph/Link.java
36,7 → 36,7
 
import net.jcip.annotations.ThreadSafe;
 
import org.jdom.Element;
import org.jdom2.Element;
 
/**
* Un lien dans le graphe des tables. Par exemple, si la table ECLAIRAGE a un champ ID_LOCAL, alors
299,7 → 299,6
final String linkName = linkElem.getAttributeValue("name");
final Rule updateRule = Rule.fromName(linkElem.getAttributeValue("updateRule"));
final Rule deleteRule = Rule.fromName(linkElem.getAttributeValue("deleteRule"));
@SuppressWarnings("unchecked")
final List<Element> lElems = linkElem.getAttribute("col") != null ? singletonList(linkElem) : linkElem.getChildren("l");
final List<SQLField> cols = new ArrayList<SQLField>();
final List<SQLField> refcols = new ArrayList<SQLField>();
/trunk/OpenConcerto/src/org/openconcerto/sql/model/mssql-functions.sql
New file
0,0 → 1,124
/********************************************************
COPYRIGHTS http://www.ranjithk.com
*********************************************************/
if OBJECT_ID('${rootName}.[CleanUpSchema]', 'P') is not null DROP PROCEDURE ${rootName}.[CleanUpSchema];
CREATE PROCEDURE ${rootName}.[CleanUpSchema]
(
@SchemaName varchar(100)
,@WorkTest char(1) = 'w' -- use 'w' to work and 't' to print
)
AS
/*-----------------------------------------------------------------------------------------
Author : Ranjith Kumar S
Date: 31/01/10
Description: It drop all the objects in a schema and then the schema itself
Limitations:
1. If a table has a PK with XML or a Spatial Index then it wont work
(workaround: drop that table manually and re run it)
2. If the schema is referred by a XML Schema collection then it wont work
If it is helpful, Please send your comments ranjith_842@hotmail.com or visit http://www.ranjithk.com
-------------------------------------------------------------------------------------------*/
BEGIN
declare @SQL varchar(4000)
declare @msg varchar(500)
IF OBJECT_ID('tempdb..#dropcode') IS NOT NULL DROP TABLE #dropcode
CREATE TABLE #dropcode
(
ID int identity(1,1)
,SQLstatement varchar(1000)
)
-- removes all the foreign keys that reference a PK in the target schema
SELECT @SQL =
'select
'' ALTER TABLE ''+SCHEMA_NAME(fk.schema_id)+''.''+QUOTENAME(OBJECT_NAME(fk.parent_object_id))+'' DROP CONSTRAINT ''+ fk.name
FROM sys.foreign_keys fk
join sys.tables t on t.object_id = fk.referenced_object_id
where t.schema_id = schema_id(''' + @SchemaName+''')
and fk.schema_id <> t.schema_id
order by fk.name desc'
IF @WorkTest = 't' PRINT (@SQL )
INSERT INTO #dropcode
EXEC (@SQL)
-- drop all default constraints, check constraints and Foreign Keys
SELECT @SQL =
'SELECT
'' ALTER TABLE ''+schema_name(t.schema_id)+''.''+QUOTENAME(OBJECT_NAME(fk.parent_object_id)) +'' DROP CONSTRAINT ''+ fk.[Name]
FROM sys.objects fk
join sys.tables t on t.object_id = fk.parent_object_id
where t.schema_id = schema_id(''' + @SchemaName+''')
and fk.type IN (''D'', ''C'', ''F'')'
IF @WorkTest = 't' PRINT (@SQL )
INSERT INTO #dropcode
EXEC (@SQL)
-- drop all other objects in order
SELECT @SQL =
'SELECT
CASE WHEN SO.type=''PK'' THEN '' ALTER TABLE ''+SCHEMA_NAME(SO.schema_id)+''.''+QUOTENAME(OBJECT_NAME(SO.parent_object_id))+'' DROP CONSTRAINT ''+ SO.name
WHEN SO.type=''U'' THEN '' DROP TABLE ''+SCHEMA_NAME(SO.schema_id)+''.''+ QUOTENAME(SO.[Name])
WHEN SO.type=''V'' THEN '' DROP VIEW ''+SCHEMA_NAME(SO.schema_id)+''.''+ QUOTENAME(SO.[Name])
WHEN SO.type=''P'' THEN '' DROP PROCEDURE ''+SCHEMA_NAME(SO.schema_id)+''.''+ QUOTENAME(SO.[Name])
WHEN SO.type=''TR'' THEN '' DROP TRIGGER ''+SCHEMA_NAME(SO.schema_id)+''.''+ QUOTENAME(SO.[Name])
WHEN SO.type IN (''FN'', ''TF'',''IF'',''FS'',''FT'') THEN '' DROP FUNCTION ''+SCHEMA_NAME(SO.schema_id)+''.''+ QUOTENAME(SO.[Name])
END
FROM SYS.OBJECTS SO
WHERE SO.schema_id = schema_id('''+ @SchemaName +''')
AND SO.type IN (''PK'', ''FN'', ''TF'', ''TR'', ''V'', ''U'', ''P'')
ORDER BY CASE WHEN type = ''PK'' THEN 1
WHEN type in (''FN'', ''TF'', ''P'',''IF'',''FS'',''FT'') THEN 2
WHEN type = ''TR'' THEN 3
WHEN type = ''V'' THEN 4
WHEN type = ''U'' THEN 5
ELSE 6
END'
IF @WorkTest = 't' PRINT (@SQL )
INSERT INTO #dropcode
EXEC (@SQL)
DECLARE @ID int, @statement varchar(1000)
DECLARE statement_cursor CURSOR
FOR SELECT SQLStatement
FROM #dropcode
ORDER BY ID ASC
OPEN statement_cursor
FETCH statement_cursor INTO @statement
WHILE (@@FETCH_STATUS = 0)
BEGIN
IF @WorkTest = 't' PRINT (@statement)
ELSE
BEGIN
PRINT (@statement)
EXEC(@statement)
END
FETCH statement_cursor INTO @statement
END
CLOSE statement_cursor
DEALLOCATE statement_cursor
IF @WorkTest = 't' PRINT ('DROP SCHEMA '+@SchemaName)
ELSE
BEGIN
PRINT ('DROP SCHEMA '+@SchemaName)
EXEC ('DROP SCHEMA '+@SchemaName)
END
PRINT '------- ALL - DONE -------'
END
/trunk/OpenConcerto/src/org/openconcerto/sql/model/Constraint.java
21,18 → 21,24
import java.util.List;
import java.util.Map;
 
import org.jdom.Element;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.Immutable;
 
import org.jdom2.Element;
 
@Immutable
public final class Constraint {
 
@SuppressWarnings("unchecked")
public static Constraint fromXML(final SQLTable t, Element elem) {
return new Constraint(t, elem.getAttributeValue("name"), (Map<String, Object>) XMLCodecUtils.decode1((Element) elem.getChildren().get(0)));
return new Constraint(t, elem.getAttributeValue("name"), (Map<String, Object>) XMLCodecUtils.decode1(elem.getChildren().get(0)));
}
 
private final SQLTable t;
private final String name;
// private copy
private final Map<String, Object> m;
@GuardedBy("this")
private String xml = null;
 
private Constraint(final SQLTable t, final String name, final Map<String, Object> row) {
43,12 → 49,20
 
Constraint(final SQLTable t, final Map<String, Object> row) {
this.t = t;
this.name = (String) row.remove("CONSTRAINT_NAME");
this.m = new HashMap<String, Object>(row);
this.name = (String) this.m.remove("CONSTRAINT_NAME");
this.m.remove("TABLE_SCHEMA");
this.m.remove("TABLE_NAME");
}
 
Constraint(final SQLTable t, final Constraint c) {
this.t = t;
this.m = c.m;
this.name = c.name;
// don't bother synchronising for xml since when we copy a Constraint it generally has never
// been used.
}
 
public final SQLTable getTable() {
return this.t;
}
71,7 → 85,7
return (List<String>) this.m.get("COLUMN_NAMES");
}
 
public String toXML() {
public synchronized String toXML() {
// this is immutable so only compute once the XML
if (this.xml == null)
this.xml = "<constraint name=\"" + JDOMUtils.OUTPUTTER.escapeAttributeEntities(getName()) + "\" >" + XMLCodecUtils.encodeSimple(this.m) + "</constraint>";
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLBase.java
27,6 → 27,7
import org.openconcerto.utils.Tuple3;
import org.openconcerto.utils.cc.CopyOnWriteMap;
import org.openconcerto.utils.cc.IClosure;
import org.openconcerto.utils.cc.ITransformer;
import org.openconcerto.utils.change.CollectionChangeEventCreator;
 
import java.io.File;
37,10 → 38,11
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
53,6 → 55,8
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;
 
import org.apache.commons.dbutils.ResultSetHandler;
 
/**
* Une base de donnée SQL. Une base est unique, pour obtenir une instance il faut passer par
* SQLServer. Une base permet d'accéder aux tables qui la composent, ainsi qu'à son graphe.
108,7 → 112,7
* @param pass the password.
*/
SQLBase(SQLServer server, String name, String login, String pass) {
this(server, name, login, pass, null);
this(server, name, null, login, pass, null);
}
 
/**
119,12 → 123,13
*
* @param server its server.
* @param name its name.
* @param systemRootInit to initialize the {@link DBSystemRoot} before setting the datasource.
* @param login the login.
* @param pass the password.
* @param dsInit to initialize the datasource before any request (eg setting jdbc properties),
* can be <code>null</code>.
*/
SQLBase(SQLServer server, String name, String login, String pass, IClosure<SQLDataSource> dsInit) {
SQLBase(SQLServer server, String name, IClosure<? super DBSystemRoot> systemRootInit, String login, String pass, IClosure<? super SQLDataSource> dsInit) {
super(server, name);
if (name == null)
throw new NullPointerException("null base");
134,7 → 139,7
// if this is the systemRoot we must init the datasource to be able to loadTables()
final DBSystemRoot sysRoot = this.getDBSystemRoot();
if (sysRoot.getJDBC() == this)
sysRoot.setDS(login, pass, dsInit);
sysRoot.setDS(systemRootInit, login, pass, dsInit);
}
 
final TablesMap init(final boolean readCache) {
149,6 → 154,7
protected synchronized void onDrop() {
// allow schemas (and their descendants) to be gc'd even we aren't
this.schemas.clear();
SQLType.remove(this);
super.onDrop();
}
 
564,74 → 570,79
*
* @param schema the name of the schema.
* @param name the name of the meta data.
* @param shouldTestForTable <code>true</code> if the method should try to test if the table
* exists, <code>false</code> to just execute a SELECT. Important for postgreSQL since an
* error aborts the whole transaction.
* @return the requested meta data, can be <code>null</code> (including if
* {@value SQLSchema#METADATA_TABLENAME} does not exist).
*/
String getFwkMetadata(String schema, String name, final boolean shouldTestForTable) {
String getFwkMetadata(String schema, String name) {
return getFwkMetadata(Collections.singletonList(schema), name).get(schema);
}
 
private final String getSel(final String schema, final String name, final boolean selSchema) {
final SQLName tableName = new SQLName(this.getName(), schema, SQLSchema.METADATA_TABLENAME);
final String sel = "SELECT \"VALUE\" FROM " + tableName.quote() + " WHERE \"NAME\"= " + this.quoteString(name);
// In postgreSQL once there's an error the transaction is aborted and further queries throw
// an exception. In H2 and MySQL, the transaction is *not* aborted.
final SQLSystem system = getServer().getSQLSystem();
if (shouldTestForTable && system == SQLSystem.POSTGRESQL) {
final String stringDel = "$sel$";
if (sel.contains(stringDel))
throw new IllegalStateException(sel + " contains string delimiter : " + stringDel);
final String funcName = SQLBase.quoteIdentifier(schema) + ".ifExistText";
final String query = "create or replace function " + funcName + "(schemaName text, tableName text, doesExist text, doesNotExist text) returns text as $BODY$\n"
// body
+ "declare res text;\nbegin\n"
//
+ " drop function " + funcName + "(text,text,text,text);\n"
//
+ " if EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = schemaName and table_name = tableName) then\n"
//
+ " execute doesExist into res; else execute doesNotExist into res;\n"
//
+ " end if;\n"
//
+ " return res;\nend;\n$BODY$ LANGUAGE plpgsql;\n"
//
+ "select " + funcName + "(" + this.quoteString(schema) + ", " + this.quoteString(SQLSchema.METADATA_TABLENAME) + ", " + stringDel + sel + stringDel + ", 'SELECT NULL')";
try {
return this.getDataSource().useConnection(new ConnectionHandlerNoSetup<String, SQLException>() {
return "SELECT " + (selSchema ? this.quoteString(schema) + ", " : "") + "\"VALUE\" FROM " + tableName.quote() + " WHERE \"NAME\"= " + this.quoteString(name);
}
 
private final void exec(final Collection<String> schemas, final String name, final ResultSetHandler rsh) {
this.getDataSource().execute(CollectionUtils.join(schemas, "\nUNION ALL ", new ITransformer<String, String>() {
@Override
public String handle(SQLDataSource ds) throws SQLException, SQLException {
final Statement stmt = ds.getConnection().createStatement();
stmt.execute(query);
if (!stmt.getMoreResults())
throw new IllegalStateException("No result");
return (String) SQLDataSource.SCALAR_HANDLER.handle(stmt.getResultSet());
public String transformChecked(String schema) {
// schema name needed since missing values will result in missing rows not
// null values
return getSel(schema, name, true);
}
});
} catch (SQLException e) {
throw new IllegalStateException(e);
}), new IResultSetHandler(rsh, false));
}
} else {
try {
return (String) this.getDataSource().execute(sel, new IResultSetHandler(SQLDataSource.SCALAR_HANDLER, false));
} catch (RuntimeException rtExn) {
// pg transactions are aborted, so let the caller know right away (better than to
// continue and fail later)
try {
if (system == SQLSystem.POSTGRESQL && this.getDataSource().handlingConnection() && !this.getDataSource().getConnection().getAutoCommit())
throw rtExn;
} catch (SQLException e) {
throw new IllegalStateException("Couldn't get auto commit : " + e.getMessage() + " " + e.getSQLState(), rtExn);
 
Map<String, String> getFwkMetadata(final Collection<String> schemas, final String name) {
if (schemas.isEmpty())
return Collections.emptyMap();
final Map<String, String> res = new LinkedHashMap<String, String>();
CollectionUtils.fillMap(res, schemas);
final ResultSetHandler rsh = new ResultSetHandler() {
@Override
public Object handle(ResultSet rs) throws SQLException {
while (rs.next()) {
res.put(rs.getString(1), rs.getString(2));
}
final SQLException sqlExn = SQLUtils.findWithSQLState(rtExn);
// table or view not found
if (sqlExn != null && (sqlExn.getSQLState().equals("42S02") || sqlExn.getSQLState().equals("42P01")))
return null;
else
throw rtExn;
}
};
try {
if (this.getDataSource().getTransactionPoint() == null) {
exec(schemas, name, rsh);
} else {
// If already in a transaction, don't risk aborting it if a table doesn't exist.
// (it's not strictly required for H2 and MySQL, since the transaction is *not*
// aborted)
SQLUtils.executeAtomic(this.getDataSource(), new ConnectionHandlerNoSetup<Object, SQLException>() {
@Override
public Object handle(SQLDataSource ds) throws SQLException {
exec(schemas, name, rsh);
return null;
}
}, false);
}
} catch (Exception exn) {
final SQLException sqlExn = SQLUtils.findWithSQLState(exn);
final boolean tableNotFound = sqlExn != null && (sqlExn.getSQLState().equals("42S02") || sqlExn.getSQLState().equals("42P01"));
if (!tableNotFound)
throw new IllegalStateException("Not a missing table exception", sqlExn);
 
// The following fall back should not currently be needed since the table is created
// by JDBCStructureSource.getNames(). Even without that most DB should contain the
// metadata tables.
 
// if only one schema, there's no ambiguity : just return null value
// otherwise retry with each single schema to find out which ones are missing
if (schemas.size() > 1) {
// this won't loop indefinetly since schemas.size() will be 1
for (final String schema : schemas)
res.put(schema, this.getFwkMetadata(schema, name));
}
}
return res;
}
 
public final String getMDName() {
return this.getServer().getSQLSystem().getMDName(this.getName());
}
836,7 → 847,7
* @return the quoted form, eg "'salut\ l''ami'".
*/
public final static String quoteStringStd(String s) {
return "'" + singleQuote.matcher(s).replaceAll("''") + "'";
return s == null ? "NULL" : "'" + singleQuote.matcher(s).replaceAll("''") + "'";
}
 
/**
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLSyntaxMySQL.java
14,15 → 14,17
package org.openconcerto.sql.model;
 
import org.openconcerto.sql.model.SQLField.Properties;
import org.openconcerto.sql.model.SQLTable.Index;
import org.openconcerto.sql.model.SQLTable.SQLIndex;
import org.openconcerto.sql.model.graph.Link.Rule;
import org.openconcerto.sql.model.graph.TablesMap;
import org.openconcerto.sql.utils.ChangeTable;
import org.openconcerto.sql.utils.ChangeTable.ClauseType;
import org.openconcerto.sql.utils.ChangeTable.OutsideClause;
import org.openconcerto.sql.utils.SQLUtils;
import org.openconcerto.sql.utils.SQLUtils.SQLFactory;
import org.openconcerto.utils.CollectionMap;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.ListMap;
import org.openconcerto.utils.StringUtils;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.utils.cc.IClosure;
import org.openconcerto.utils.cc.ITransformer;
64,18 → 66,18
 
SQLSyntaxMySQL() {
super(SQLSystem.MYSQL);
this.typeNames.putAll(Boolean.class, "boolean", "bool", "bit");
this.typeNames.putAll(Short.class, "smallint");
this.typeNames.putAll(Integer.class, "integer", "int");
this.typeNames.putAll(Long.class, "bigint");
this.typeNames.putAll(BigDecimal.class, "decimal", "numeric");
this.typeNames.putAll(Float.class, "float");
this.typeNames.putAll(Double.class, "double precision", "real");
this.typeNames.putAll(Timestamp.class, "timestamp");
this.typeNames.putAll(java.util.Date.class, "time");
this.typeNames.putAll(Blob.class, "blob", "tinyblob", "mediumblob", "longblob", "varbinary", "binary");
this.typeNames.putAll(Clob.class, "text", "tinytext", "mediumtext", "longtext", "varchar", "char");
this.typeNames.putAll(String.class, "varchar", "char");
this.typeNames.addAll(Boolean.class, "boolean", "bool", "bit");
this.typeNames.addAll(Short.class, "smallint");
this.typeNames.addAll(Integer.class, "integer", "int");
this.typeNames.addAll(Long.class, "bigint");
this.typeNames.addAll(BigDecimal.class, "decimal", "numeric");
this.typeNames.addAll(Float.class, "float");
this.typeNames.addAll(Double.class, "double precision", "real");
this.typeNames.addAll(Timestamp.class, "timestamp");
this.typeNames.addAll(java.util.Date.class, "time");
this.typeNames.addAll(Blob.class, "blob", "tinyblob", "mediumblob", "longblob", "varbinary", "binary");
this.typeNames.addAll(Clob.class, "text", "tinytext", "mediumtext", "longtext", "varchar", "char");
this.typeNames.addAll(String.class, "varchar", "char");
}
 
public String getIDType() {
103,6 → 105,12
}
 
@Override
public int getMaximumVarCharLength() {
// http://dev.mysql.com/doc/refman/5.0/en/char.html
return (65535 - 2) / SQLSyntaxPG.MAX_BYTES_PER_CHAR;
}
 
@Override
protected Tuple2<Boolean, String> getCast() {
return null;
}
115,7 → 123,7
@Override
public String transfDefaultJDBC2SQL(SQLField f) {
final Class<?> javaType = f.getType().getJavaType();
String res = (String) f.getDefaultValue();
String res = f.getDefaultValue();
if (res == null)
// either no default or NULL default
// see http://dev.mysql.com/doc/refman/5.0/en/data-type-defaults.html
190,13 → 198,21
}
 
@Override
protected String getCreateIndex(String cols, SQLName tableName, Index i) {
protected String getCreateIndex(String cols, SQLName tableName, SQLIndex i) {
final String method = i.getMethod() != null ? " USING " + i.getMethod() : "";
return super.getCreateIndex(cols, tableName, i) + method;
}
 
@Override
public List<String> getAlterField(SQLField f, Set<Properties> toAlter, String type, String defaultVal, Boolean nullable) {
public boolean isUniqueException(SQLException exn) {
final SQLException e = SQLUtils.findWithSQLState(exn);
// 1062 is the real "Duplicate entry" error, 1305 happens when we emulate partial unique
// constraint
return e.getErrorCode() == 1062 || (e.getErrorCode() == 1305 && e.getMessage().contains(ChangeTable.MYSQL_FAKE_PROCEDURE + " does not exist"));
}
 
@Override
public Map<ClauseType, List<String>> getAlterField(SQLField f, Set<Properties> toAlter, String type, String defaultVal, Boolean nullable) {
final boolean newNullable = toAlter.contains(Properties.NULLABLE) ? nullable : getNullable(f);
final String newType = toAlter.contains(Properties.TYPE) ? type : getType(f);
String newDef = toAlter.contains(Properties.DEFAULT) ? defaultVal : getDefault(f, newType);
204,7 → 220,7
if (!newNullable && newDef != null && newDef.trim().toUpperCase().equals("NULL"))
newDef = null;
 
return Collections.singletonList("MODIFY COLUMN " + f.getQuotedName() + " " + getFieldDecl(newType, newDef, newNullable));
return ListMap.singleton(ClauseType.ALTER_COL, "MODIFY COLUMN " + f.getQuotedName() + " " + getFieldDecl(newType, newDef, newNullable));
}
 
@Override
218,14 → 234,14
}
 
@Override
protected void _storeData(final SQLTable t, final File file) {
protected void _storeData(final SQLTable t, final File file) throws IOException {
checkServerLocalhost(t);
final CollectionMap<String, String> charsets = new CollectionMap<String, String>();
final ListMap<String, String> charsets = new ListMap<String, String>();
for (final SQLField f : t.getFields()) {
final Object charset = f.getInfoSchema().get("CHARACTER_SET_NAME");
// non string field
if (charset != null)
charsets.put(charset, f.getName());
charsets.add(charset.toString(), f.getName());
}
if (charsets.size() > 1)
// MySQL dumps strings in binary, so fields must be consistent otherwise the
240,9 → 256,10
return base.quoteString(input.getName());
}
});
try {
final File tmp = File.createTempFile(SQLSyntaxMySQL.class.getSimpleName() + "storeData", ".txt");
// mysql cannot overwrite files
// MySQL cannot overwrite files. Also on Windows tmp is in the user profile which the
// service cannot access ; conversely tmpdir of MySQL is not readable by normal users,
// in that case grant traverse and write permission to MySQL (e.g. Network Service).
tmp.delete();
final SQLSelect sel = new SQLSelect(true).addSelectStar(t);
// store the data in the temp file
249,13 → 266,15
base.getDataSource().execute("SELECT " + cols + " UNION " + sel.asString() + " INTO OUTFILE " + base.quoteString(tmp.getAbsolutePath()) + " " + getDATA_OPTIONS(base) + ";");
// then read it to remove superfluous escape char and convert to utf8
final BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(tmp), charset));
final Writer w = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF8"));
Writer w = null;
try {
w = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StringUtils.UTF8));
normalizeData(r, w, 1000 * 1024);
} finally {
r.close();
if (w != null)
w.close();
tmp.delete();
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
 
329,8 → 348,8
}
 
@Override
public SQLBase createBase(SQLServer server, String name, String login, String pass, IClosure<SQLDataSource> dsInit) {
return new MySQLBase(server, name, login, pass, dsInit);
SQLBase createBase(SQLServer server, String name, final IClosure<? super DBSystemRoot> systemRootInit, String login, String pass, IClosure<? super SQLDataSource> dsInit) {
return new MySQLBase(server, name, systemRootInit, login, pass, dsInit);
}
 
@Override
423,7 → 442,7
@Override
@SuppressWarnings("unchecked")
public List<Map<String, Object>> getConstraints(SQLBase b, TablesMap tables) throws SQLException {
final String sel = "SELECT null as \"TABLE_SCHEMA\", c.\"TABLE_NAME\", c.\"CONSTRAINT_NAME\", tc.\"CONSTRAINT_TYPE\", \"COLUMN_NAME\", c.\"ORDINAL_POSITION\"\n"
final String sel = "SELECT null as \"TABLE_SCHEMA\", c.\"TABLE_NAME\", c.\"CONSTRAINT_NAME\", tc.\"CONSTRAINT_TYPE\", \"COLUMN_NAME\", c.\"ORDINAL_POSITION\", NULL as \"DEFINITION\"\n"
// from
+ " FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE c\n"
// "-- sub-select otherwise at least 15s\n" +
/trunk/OpenConcerto/src/org/openconcerto/sql/model/DBSystemRoot.java
97,8 → 97,6
}
};
this.loadingListenersSupp = new LoadingChangeSupport(this);
 
this.getServer().init(this);
}
 
private synchronized void rootsChanged(PropertyChangeEvent evt) {
583,10 → 581,12
return res;
}
 
synchronized final void setDS(String login, String pass, IClosure<? super SQLDataSource> dsInit) {
synchronized final void setDS(IClosure<? super DBSystemRoot> systemRootInit, String login, String pass, IClosure<? super SQLDataSource> dsInit) {
if (this.ds != null)
throw new IllegalStateException("already set: " + this.ds);
assert this.dsInit == null;
if (systemRootInit != null)
systemRootInit.executeChecked(this);
this.dsInit = dsInit;
this.ds = createDSUnsafe(login, pass);
 
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLName.java
23,14 → 23,18
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
import net.jcip.annotations.Immutable;
 
/**
* A dotted SQL name, eg "table.field" or "schema.table".
*
* @author Sylvain
*/
@Immutable
public final class SQLName {
 
private static final Pattern unquoted = Pattern.compile("\\w+");
private static final Pattern MS_END_QUOTE = Pattern.compile("]", Pattern.LITERAL);
 
/**
* Parse a possibly quoted string to an SQL name.
39,25 → 43,34
* @return the corresponding SQL name, eg "public"."ta.ble seq".
*/
public static SQLName parse(String name) {
return parse(name, '"', '"');
}
 
public static SQLName parseMS(String name) {
// lucky for us, the rules are the same as for standard SQL
return parse(name, '[', ']');
}
 
private static SQLName parse(String name, final char startQuote, final char endQuote) {
name = name.trim();
final List<String> res = new ArrayList<String>();
int index = 0;
while (index < name.length()) {
final char c = name.charAt(index);
final boolean inQuote = c == '"';
final boolean inQuote = c == startQuote;
if (inQuote) {
// pass the opening quote
index += 1;
int index2 = findNextQuote(name, index);
int index2 = findNextQuote(name, index, endQuote);
// handle escaped "
String part = "";
// while the char after " is also "
while ((index2 + 1) < name.length() && name.charAt(index2 + 1) == '"') {
while ((index2 + 1) < name.length() && name.charAt(index2 + 1) == endQuote) {
// index2+1 to keep the first quote
part += name.substring(index, index2 + 1);
// pass ""
index = index2 + 2;
index2 = findNextQuote(name, index);
index2 = findNextQuote(name, index, endQuote);
}
part += name.substring(index, index2);
res.add(part);
84,14 → 97,14
return new SQLName(res);
}
 
private static int findNextQuote(final String name, final int index) {
final int res = name.indexOf('"', index);
private static int findNextQuote(final String name, final int index, final char c) {
final int res = name.indexOf(c, index);
if (res < 0)
throw new IllegalArgumentException("no corresponding quote " + index);
return res;
}
 
final List<String> items;
private final List<String> items;
 
public SQLName(String... items) {
this(Arrays.asList(items));
105,13 → 118,22
* @param items the names.
*/
public SQLName(List<String> items) {
this(items, false);
}
 
private SQLName(List<String> items, final boolean safe) {
super();
this.items = new ArrayList<String>(items.size());
if (safe) {
this.items = items;
} else {
final List<String> tmp = new ArrayList<String>(items.size());
for (final String item : items) {
if (item != null && item.length() > 0)
this.items.add(item);
tmp.add(item);
}
this.items = Collections.unmodifiableList(tmp);
}
}
 
/**
* Return the quoted form, eg for table.field : "table"."field".
126,6 → 148,14
});
}
 
public String quoteMS() {
return CollectionUtils.join(this.items, ".", new ITransformer<String, String>() {
public String transformChecked(String input) {
return '[' + MS_END_QUOTE.matcher(input).replaceAll("]]") + ']';
}
});
}
 
/**
* Return the item at the given index. You can use negatives to count backwards (ie -1 is the
* last item).
171,7 → 201,7
}
 
public SQLName getRest() {
return new SQLName(this.items.subList(1, this.items.size()));
return new SQLName(this.items.subList(1, this.items.size()), true);
}
 
/**
190,7 → 220,7
final List<String> l = new ArrayList<String>(fromCount);
l.addAll(from.asList().subList(0, fromCount - toCount));
l.addAll(to.asList());
return new SQLName(l);
return new SQLName(Collections.unmodifiableList(l), true);
}
}
 
216,12 → 246,12
if (common == 0) {
return to;
} else {
return new SQLName(to.asList().subList(common, toCount));
return new SQLName(to.asList().subList(common, toCount), true);
}
}
 
public final List<String> asList() {
return Collections.unmodifiableList(this.items);
return this.items;
}
 
@Override
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLRowListRSH.java
18,6 → 18,7
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
 
import org.apache.commons.dbutils.ResultSetHandler;
25,9 → 26,18
public final class SQLRowListRSH implements ResultSetHandler {
 
// hashCode()/equals() needed for data source cache
private static final class RSH implements ResultSetHandler {
public static final class RSH implements ResultSetHandler {
private final Tuple2<SQLTable, List<String>> names;
 
// allow to create rows from arbitrary columns (and not just directly from actual fields of
// the same table)
// ATTN doesn't check that the types of columns are coherent with the types of the fields
public RSH(final SQLTable t, final List<String> names) {
this(Tuple2.create(t, names));
if (!t.getFieldsName().containsAll(names))
throw new IllegalArgumentException("Not all names are fields of " + t + " : " + names);
}
 
private RSH(Tuple2<SQLTable, List<String>> names) {
this.names = names;
}
34,7 → 44,9
 
@Override
public List<SQLRow> handle(ResultSet rs) throws SQLException {
return SQLRow.createListFromRS(this.names.get0(), rs, this.names.get1());
// since the result will be cached, disallow its modification (e.g.avoid
// ConcurrentModificationException)
return Collections.unmodifiableList(SQLRow.createListFromRS(this.names.get0(), rs, this.names.get1()));
}
 
@Override
55,38 → 67,52
}
}
 
private static Tuple2<SQLTable, List<String>> getIndexes(SQLSelect sel, final SQLTable passedTable, final boolean findTable) {
final List<SQLField> selectFields = sel.getSelectFields();
private static TableRef checkTable(final TableRef t) {
if (t == null)
throw new IllegalArgumentException("null table");
if (!t.getTable().isRowable())
throw new IllegalArgumentException("table isn't rowable : " + t);
return t;
}
 
private static Tuple2<SQLTable, List<String>> getIndexes(SQLSelect sel, final TableRef passedTable, final boolean findTable) {
final List<FieldRef> selectFields = sel.getSelectFields();
final int size = selectFields.size();
if (size == 0)
throw new IllegalArgumentException("empty select : " + sel);
final SQLTable t;
TableRef t;
if (findTable) {
if (passedTable != null)
throw new IllegalArgumentException("non null table " + passedTable);
t = selectFields.get(0).getTable();
t = null;
} else {
if (passedTable == null)
throw new IllegalArgumentException("null table");
t = passedTable;
t = checkTable(passedTable);
}
// cannot pass an alias to this method since getSelectFields() returns SQLField and not
// FieldRef
final List<TableRef> aliases = sel.getAliases(t);
if (aliases.size() != 1)
throw new IllegalArgumentException(t + " isn't exactly once : " + aliases);
final List<String> l = new ArrayList<String>(size);
for (int i = 0; i < size; i++) {
final SQLField field = selectFields.get(i);
if (field.getTable().equals(t))
l.add(field.getName());
else if (findTable)
final FieldRef field = selectFields.get(i);
if (field == null) {
// computed field
l.add(null);
} else {
if (t == null) {
assert findTable;
t = checkTable(field.getTableRef());
}
assert t != null && t.getTable().isRowable();
 
if (field.getTableRef().equals(t)) {
l.add(field.getField().getName());
} else if (findTable) {
// prevent ambiguity : either specify a table or there must be only one table
throw new IllegalArgumentException(field + " is not in " + t);
else
} else {
l.add(null);
}
return Tuple2.create(t, l);
}
}
return Tuple2.create(t.getTable(), l);
}
 
/**
* Create a handler that don't need metadata.
103,10 → 129,10
* each metadata.
*
* @param sel the select that will produce the result set.
* @param t the table for which to create rows, must appear only once in <code>sel</code>.
* @param t the table for which to create rows.
* @return a handler creating a list of {@link SQLRow}.
*/
static public ResultSetHandler createFromSelect(final SQLSelect sel, final SQLTable t) {
static public ResultSetHandler createFromSelect(final SQLSelect sel, final TableRef t) {
return create(getIndexes(sel, t, false));
}
 
/trunk/OpenConcerto/src/org/openconcerto/sql/model/XMLStructureSource.java
25,6 → 25,7
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
33,9 → 34,9
import java.util.Map.Entry;
import java.util.Set;
 
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.input.SAXBuilder;
 
public class XMLStructureSource extends StructureSource<IOException> {
 
43,7 → 44,7
* Date format used in xml files.
*/
public static final DateFormat XMLDATE_FMT = new SimpleDateFormat("yyyyMMdd-HHmmss.SSSZ");
public static final String version = "20140213-1231";
public static final String version = "20141001-1155";
 
private final Map<String, Element> xmlSchemas;
 
88,14 → 89,21
String problems = "";
final TablesMap outOfDateTables = new TablesMap();
final SAXBuilder sxb = new SAXBuilder();
final Set<String> schemaNamesToLoad = new HashSet<String>();
final List<DBItemFileCache> schemaFilesToLoad = new ArrayList<DBItemFileCache>();
for (final DBItemFileCache savedSchema : this.dir.getSavedDesc(SQLSchema.class, SQLBase.FILENAME)) {
final String schemaName = savedSchema.getName();
// ignore out of scope for this refresh and inexistent schemas
if (!this.allSchemas.contains(schemaName) || !this.isInScope(schemaName))
continue;
schemaFilesToLoad.add(savedSchema);
schemaNamesToLoad.add(schemaName);
}
final Map<String, String> schemaDBVersions = SQLSchema.getVersions(this.getBase(), schemaNamesToLoad);
for (final DBItemFileCache savedSchema : schemaFilesToLoad) {
final String schemaName = savedSchema.getName();
final String schemaDBVersion = schemaDBVersions.get(schemaName);
 
final String schemaDBVersion = SQLSchema.getVersion(this.getBase(), schemaName);
 
final File schemaFile = savedSchema.getFile(SQLBase.FILENAME);
String schemaProblem = "";
Element schemaElem = null;
140,9 → 148,7
 
this.schemas.add(schemaName);
final IncludeExclude<String> tablesToRefresh = this.getTablesInScope(schemaName);
final List l = schemaElem.getChildren("table");
for (int i = 0; i < l.size(); i++) {
final Element elementTable = (Element) l.get(i);
for (final Element elementTable : schemaElem.getChildren("table")) {
final String tableName = elementTable.getAttributeValue("name");
if (tablesToRefresh.isIncluded(tableName)) {
if (isVersionBad(SQLSchema.getVersion(elementTable), schemaDBVersion).length() == 0)
153,6 → 159,7
}
}
}
 
if (problems.length() > 0)
SQLBase.logCacheError(this.dir, new IllegalStateException("invalid files : " + problems));
if (outOfDateTables.size() > 0)
/trunk/OpenConcerto/src/org/openconcerto/sql/model/Trigger.java
13,31 → 13,51
package org.openconcerto.sql.model;
 
import org.openconcerto.xml.JDOMUtils;
import org.openconcerto.xml.XMLCodecUtils;
 
import java.util.HashMap;
import java.util.Map;
 
import org.jdom.Element;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.Immutable;
 
import org.jdom2.Element;
 
@Immutable
public final class Trigger {
 
@SuppressWarnings("unchecked")
public static Trigger fromXML(final SQLTable t, Element elem) {
return new Trigger(t, (Map<String, Object>) XMLCodecUtils.decode1((Element) elem.getChildren().get(0)));
return new Trigger(t, elem.getAttributeValue("name"), (Map<String, Object>) XMLCodecUtils.decode1(elem.getChildren().get(0)));
}
 
private final SQLTable t;
private final String name;
private final Map<String, Object> m;
private String xml;
@GuardedBy("this")
private String xml = null;
 
Trigger(final SQLTable t, final Map<String, Object> row) {
private Trigger(final SQLTable t, final String name, final Map<String, Object> row) {
this.t = t;
this.name = (String) row.get("TRIGGER_NAME");
this.name = name;
this.m = row;
this.xml = null;
}
 
Trigger(final SQLTable t, final Map<String, Object> row) {
this.t = t;
this.m = new HashMap<String, Object>(row);
this.name = (String) this.m.remove("TRIGGER_NAME");
}
 
Trigger(final SQLTable t, final Trigger trigger) {
this.t = t;
this.m = trigger.m;
this.name = trigger.name;
// don't bother synchronising for xml since when we copy a Trigger it generally has never
// been used.
}
 
public final SQLTable getTable() {
return this.t;
}
55,10 → 75,14
return (String) this.m.get("SQL");
}
 
public String toXML() {
public final String getAction() {
return (String) this.m.get("ACTION");
}
 
public synchronized String toXML() {
// this is immutable so only compute once the XML
if (this.xml == null)
this.xml = "<trigger>" + XMLCodecUtils.encodeSimple(this.m) + "</trigger>";
this.xml = "<trigger name=\"" + JDOMUtils.OUTPUTTER.escapeAttributeEntities(getName()) + "\">" + XMLCodecUtils.encodeSimple(this.m) + "</trigger>";
return this.xml;
}
 
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLSystem.java
284,6 → 284,19
public boolean autoCreatesFKIndex() {
return false;
}
 
@Override
public boolean isIndexFilterConditionSupported() {
return true;
}
 
@Override
public boolean isTablesCommentSupported() {
// comments are not directly supported in MS, see sp_addextendedproperty
// 'MS_Description' :
// http://stackoverflow.com/questions/378700/is-it-possible-to-add-a-description-comment-to-a-table-in-microsoft-sql-2000
return false;
}
},
DERBY("Apache Derby");
 
/trunk/OpenConcerto/src/org/openconcerto/sql/model/JDBCStructureSource.java
18,7 → 18,6
import org.openconcerto.sql.model.SystemQueryExecutor.QueryExn;
import org.openconcerto.sql.model.graph.TablesMap;
import org.openconcerto.sql.utils.SQLCreateMoveableTable;
import org.openconcerto.utils.CollectionMap;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.utils.cc.ITransformer;
30,7 → 29,6
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
47,12 → 45,12
}
 
private final Set<String> schemas;
private final CollectionMap<SQLName, String> tableNames;
private final Map<SQLName, List<String>> tableNames;
 
public JDBCStructureSource(SQLBase b, TablesMap scope, Map<String, SQLSchema> newStruct, Set<String> outOfDateSchemas) {
super(b, scope, newStruct, outOfDateSchemas);
this.schemas = new HashSet<String>();
this.tableNames = new CollectionMap<SQLName, String>(new ArrayList<String>(2));
this.tableNames = new HashMap<SQLName, List<String>>();
// if we can't access the metadata directly from the base, obviously the base is not ok
this.setPreVerify(false);
}
75,7 → 73,7
// don't use getSchemas() since we can't limit to a particular db and it returns db private
// schemas
// les tables de la base
final CollectionMap<SQLName, String> tableNames = new CollectionMap<SQLName, String>(new ArrayList<String>(2));
final Map<SQLName, List<String>> tableNames = new HashMap<SQLName, List<String>>();
 
// to find empty schemas (with no tables) : all schemas - system schemas
final Set<String> schemas = this.getJDBCSchemas(metaData);
97,7 → 95,9
if (tablesToRefresh.isIncluded(tableName)) {
// MySQL needs this.addConnectionProperty("useInformationSchema", "true");
// but the time goes from 3.5s to 20s
tableNames.putAll(new SQLName(schemaName, tableName), asList(rs.getString("TABLE_TYPE"), rs.getString("REMARKS")));
final List<String> prev = tableNames.put(new SQLName(schemaName, tableName), asList(rs.getString("TABLE_TYPE"), rs.getString("REMARKS")));
if (prev != null)
throw new IllegalStateException(tableName + " already loaded in " + schemaName);
}
}
}
131,7 → 131,7
if (stmt == null)
stmt = conn.createStatement();
stmt.execute(createMetadata.asString(rootName));
tableNames.putAll(md, asList("TABLE", ""));
tableNames.put(md, asList("TABLE", ""));
} else if (useCache) {
Log.get().warning(getCacheError(rootName));
}
171,6 → 171,9
protected void _fillTables(final TablesMap newSchemas, final Connection conn) throws SQLException {
final boolean useCache = getBase().getDBSystemRoot().useCache();
 
// always fetch version to record in tables since we might decide to use cache later
final Map<String, String> schemaVersions = SQLSchema.getVersions(getBase(), newSchemas.keySet());
 
// for new tables, add ; for existing, refresh
final DatabaseMetaData metaData = conn.getMetaData();
// getColumns() only supports pattern (eg LIKE) so we must make multiple calls
178,7 → 181,7
final SQLSchema schema = getNewSchema(s);
 
// always fetch version to record in tables since we might decide to use cache later
String schemaVers = SQLSchema.getVersion(getBase(), s);
String schemaVers = schemaVersions.get(s);
// shouldn't happen since we insert the version with SQLSchema.getCreateMetadata()
if (schemaVers == null) {
// don't create table here, but again it should already be created
231,7 → 234,7
// type & comment
for (final SQLName tName : getTablesNames()) {
final SQLTable t = getNewSchema(tName.getItemLenient(-2)).getTable(tName.getName());
final List<String> l = (List<String>) this.tableNames.getNonNull(tName);
final List<String> l = this.tableNames.get(tName);
t.setType(l.get(0));
t.setComment(l.get(1));
}
253,7 → 256,10
 
final String schemaName = (String) rsMap.get("PROCEDURE_SCHEM");
if (newSchemas.containsKey(schemaName)) {
final String procName = (String) rsMap.get("PROCEDURE_NAME");
final String rawRrocName = (String) rsMap.get("PROCEDURE_NAME");
// MS always return an integer at the end, see :
// http://sourceforge.net/p/jtds/bugs/261/
final String procName = system.equals(SQLSystem.MSSQL) ? rawRrocName.substring(0, rawRrocName.lastIndexOf(';')) : rawRrocName;
Map<String, String> map = proceduresBySchema.get(schemaName);
if (map == null) {
map = new HashMap<String, String>();
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLTable.java
14,6 → 14,7
package org.openconcerto.sql.model;
 
import org.openconcerto.sql.Log;
import org.openconcerto.sql.model.SQLSelect.ArchiveMode;
import org.openconcerto.sql.model.SQLSyntax.ConstraintType;
import org.openconcerto.sql.model.SQLTableEvent.Mode;
import org.openconcerto.sql.model.graph.DatabaseGraph;
23,15 → 24,16
import org.openconcerto.sql.request.UpdateBuilder;
import org.openconcerto.sql.utils.ChangeTable;
import org.openconcerto.sql.utils.SQLCreateMoveableTable;
import org.openconcerto.utils.CollectionMap;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.ExceptionUtils;
import org.openconcerto.utils.ListMap;
import org.openconcerto.utils.SetMap;
import org.openconcerto.utils.StringUtils;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.utils.Tuple3;
import org.openconcerto.utils.Value;
import org.openconcerto.utils.cc.CopyOnWriteMap;
import org.openconcerto.utils.cc.IPredicate;
import org.openconcerto.utils.change.CollectionChangeEventCreator;
import org.openconcerto.xml.JDOMUtils;
 
54,11 → 56,13
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
import net.jcip.annotations.GuardedBy;
 
import org.apache.commons.dbutils.ResultSetHandler;
import org.jdom.Element;
import org.jdom2.Element;
 
/**
* Une table SQL. Connait ses champs, notamment sa clef primaire et ses clefs externes. Une table
132,6 → 136,12
return UNDEFINED_IDs.get(schema);
}
 
static final void removeUndefID(SQLSchema s) {
synchronized (UNDEFINED_IDs) {
UNDEFINED_IDs.remove(s);
}
}
 
static final Tuple2<Boolean, Number> getUndefID(SQLSchema b, String tableName) {
synchronized (UNDEFINED_IDs) {
final Map<String, Number> map = getUndefIDs(b);
354,7 → 364,6
 
// * from XML
 
@SuppressWarnings("unchecked")
void loadFields(Element xml) {
synchronized (this) {
this.version = SQLSchema.getVersion(xml);
361,7 → 370,7
}
 
final LinkedHashMap<String, SQLField> newFields = new LinkedHashMap<String, SQLField>();
for (final Element elementField : (List<Element>) xml.getChildren("field")) {
for (final Element elementField : xml.getChildren("field")) {
final SQLField f = SQLField.create(this, elementField);
newFields.put(f.getName(), f);
}
368,7 → 377,7
 
final Element primary = xml.getChild("primary");
final List<String> newPrimaryKeys = new ArrayList<String>();
for (final Element elementField : (List<Element>) primary.getChildren("field")) {
for (final Element elementField : primary.getChildren("field")) {
final String fieldName = elementField.getAttributeValue("name");
newPrimaryKeys.add(fieldName);
}
379,7 → 388,7
 
final Element triggersElem = xml.getChild("triggers");
if (triggersElem != null)
for (final Element triggerElem : (List<Element>) triggersElem.getChildren()) {
for (final Element triggerElem : triggersElem.getChildren()) {
this.addTrigger(Trigger.fromXML(this, triggerElem));
}
 
387,7 → 396,7
if (constraintsElem == null)
this.addConstraint((Constraint) null);
else
for (final Element elem : (List<Element>) constraintsElem.getChildren()) {
for (final Element elem : constraintsElem.getChildren()) {
this.addConstraint(Constraint.fromXML(this, elem));
}
 
539,12 → 548,16
this.clearNonPersistent();
this.version = table.version;
this.setState(table.fields, table.getPKsNames(), table.undefinedID);
this.triggers.putAll(table.triggers);
if (table.constraints == null)
for (final Trigger t : table.triggers.values()) {
this.addTrigger(new Trigger(this, t));
}
if (table.constraints == null) {
this.constraints = null;
else {
this.constraints.addAll(table.constraints);
} else {
for (final Constraint c : table.constraints) {
this.constraints.add(new Constraint(this, c));
}
}
this.setType(table.getType());
this.setComment(table.getComment());
}
559,7 → 572,7
return (m instanceof LinkedHashMap);
}
 
private synchronized void setState(Map<String, SQLField> fields, final List<String> primaryKeys, final Integer undef) {
private void setState(Map<String, SQLField> fields, final List<String> primaryKeys, final Integer undef) {
assert isOrdered(fields);
// checks new fields' table (don't use ==, see below)
for (final SQLField newField : fields.values()) {
643,8 → 656,11
}
 
/**
* The CHECK and UNIQUE constraints on this table. This is useful since FOREIGN KEY and PRIMARY
* KEY are already available through {@link #getForeignKeys()} and {@link #getPrimaryKeys()}.
* The CHECK and UNIQUE constraints on this table. This is useful since types
* {@link ConstraintType#FOREIGN_KEY FOREIGN_KEY} and {@link ConstraintType#PRIMARY_KEY
* PRIMARY_KEY} are already available through {@link #getForeignKeys()} and
* {@link #getPrimaryKeys()} ; type {@link ConstraintType#DEFAULT DEFAULT} through
* {@link SQLField#getDefaultValue()}.
*
* @return the constraints or <code>null</code> if they couldn't be retrieved.
*/
653,7 → 669,7
return null;
final Set<Constraint> res = new HashSet<Constraint>();
for (final Constraint c : this.constraints) {
if (c.getType() != ConstraintType.FOREIGN_KEY && c.getType() != ConstraintType.PRIMARY_KEY) {
if (c.getType() == ConstraintType.CHECK || c.getType() == ConstraintType.UNIQUE) {
res.add(c);
}
}
810,6 → 826,79
return new HashSet<SQLField>(this.fields.values());
}
 
static public enum VirtualFields {
ORDER {
@Override
public Set<SQLField> getFields(SQLTable t) {
final SQLField orderField = t.getOrderField();
return orderField == null ? Collections.<SQLField> emptySet() : Collections.singleton(orderField);
}
},
ARCHIVE {
@Override
Set<SQLField> getFields(SQLTable t) {
final SQLField f = t.getArchiveField();
return f == null ? Collections.<SQLField> emptySet() : Collections.singleton(f);
}
},
METADATA {
@Override
Set<SQLField> getFields(SQLTable t) {
final Set<SQLField> res = new HashSet<SQLField>(4);
res.add(t.getCreationDateField());
res.add(t.getCreationUserField());
res.add(t.getModifDateField());
res.add(t.getModifUserField());
res.remove(null);
return res;
}
},
PRIMARY_KEY {
@Override
Set<SQLField> getFields(SQLTable t) {
return t.getPrimaryKeys();
}
},
FOREIGN_KEYS {
@Override
public Set<SQLField> getFields(SQLTable t) {
return t.getForeignKeys();
}
};
 
abstract Set<SQLField> getFields(final SQLTable t);
}
 
public final Set<SQLField> getFields(final VirtualFields vf) {
return vf.getFields(this);
}
 
public final Set<SQLField> getFields(final Set<VirtualFields> vf) {
final Set<SQLField> res = new HashSet<SQLField>();
for (final VirtualFields v : vf) {
res.addAll(this.getFields(v));
}
return res;
}
 
public final Set<String> getFieldsNames(final Set<VirtualFields> vf) {
final Set<String> res = new HashSet<String>();
for (final VirtualFields v : vf) {
for (final SQLField f : this.getFields(v)) {
res.add(f.getName());
}
}
return res;
}
 
public final Set<SQLField> getFieldsExcluding(final Set<VirtualFields> vf) {
final Set<SQLField> res = getFields();
for (final VirtualFields v : vf) {
res.removeAll(this.getFields(v));
}
return res;
}
 
/**
* Retourne les champs du contenu de cette table. C'est à dire ni la clef primaire, ni les
* champs d'archive et d'ordre.
826,10 → 915,7
res.remove(this.getArchiveField());
res.remove(this.getOrderField());
if (!includeMetadata) {
res.remove(this.getCreationDateField());
res.remove(this.getCreationUserField());
res.remove(this.getModifDateField());
res.remove(this.getModifUserField());
res.removeAll(this.getFields(VirtualFields.METADATA));
}
return res;
}
884,8 → 970,13
}
 
public int getRowCount(final boolean includeUndefined) {
return this.getRowCount(includeUndefined, ArchiveMode.BOTH);
}
 
public int getRowCount(final boolean includeUndefined, final ArchiveMode archiveMode) {
final SQLSelect sel = new SQLSelect(true).addSelectFunctionStar("count").addFrom(this);
sel.setExcludeUndefined(!includeUndefined);
sel.setArchivedPolicy(archiveMode);
final Number count = (Number) this.getBase().getDataSource().execute(sel.asString(), new IResultSetHandler(SQLDataSource.SCALAR_HANDLER, false));
return count.intValue();
}
899,16 → 990,16
return this.getMaxOrder(true);
}
 
synchronized BigDecimal getMaxOrder(Boolean useCache) {
if (!this.isOrdered())
public BigDecimal getMaxOrder(Boolean useCache) {
final SQLField orderField = this.getOrderField();
if (orderField == null)
throw new IllegalStateException(this + " is not ordered");
 
final SQLSelect sel = new SQLSelect(true).addSelect(this.getOrderField(), "max");
final SQLSelect sel = new SQLSelect(true).addSelect(orderField, "max");
try {
final BigDecimal maxOrder = (BigDecimal) this.getBase().getDataSource().execute(sel.asString(), new IResultSetHandler(SQLDataSource.SCALAR_HANDLER, useCache));
return maxOrder == null ? BigDecimal.ONE.negate() : maxOrder;
} catch (ClassCastException e) {
throw new IllegalStateException(this.getOrderField().getSQLName() + " must be " + SQLSyntax.get(this).getOrderDefinition(), e);
throw new IllegalStateException(orderField.getSQLName() + " must be " + SQLSyntax.get(this).getOrderDefinition(), e);
}
}
 
1464,8 → 1555,6
final boolean checkComment = otherSystem == null || this.getServer().getSQLSystem().isTablesCommentSupported() && otherSystem.isTablesCommentSupported();
if (checkComment && !CompareUtils.equals(this.getComment(), o.getComment()))
return "comment unequal : '" + this.getComment() + "' != '" + o.getComment() + "'";
if (!CompareUtils.equals(this.getConstraints(), o.getConstraints()))
return "constraints unequal : '" + this.getConstraints() + "' != '" + o.getConstraints() + "'";
return this.equalsChildren(o, otherSystem);
}
 
1499,21 → 1588,59
return "unequal delete rule for " + l + ": " + l.getDeleteRule() + " != " + ol.getDeleteRule();
}
 
// indexes
final Set<Constraint> thisConstraints;
final Set<Constraint> otherConstraints;
try {
final Tuple2<Set<Constraint>, Set<Index>> thisConstraintsAndIndexes = this.getConstraintsAndIndexes();
final Tuple2<Set<Constraint>, Set<Index>> otherConstraintsAndIndexes = o.getConstraintsAndIndexes();
// order irrelevant
final Set<Index> thisIndexesSet = new HashSet<Index>(this.getIndexes());
final Set<Index> oIndexesSet = new HashSet<Index>(o.getIndexes());
final Set<Index> thisIndexesSet = thisConstraintsAndIndexes.get1();
final Set<Index> oIndexesSet = otherConstraintsAndIndexes.get1();
if (!thisIndexesSet.equals(oIndexesSet))
return "indexes differences: " + thisIndexesSet + "\n" + oIndexesSet;
thisConstraints = thisConstraintsAndIndexes.get0();
otherConstraints = otherConstraintsAndIndexes.get0();
} catch (SQLException e) {
// MAYBE fetch indexes with the rest to avoid exn now
return "couldn't get indexes: " + ExceptionUtils.getStackTrace(e);
}
if (!CompareUtils.equals(thisConstraints, otherConstraints))
return "constraints unequal : '" + thisConstraints + "' != '" + otherConstraints + "'";
 
return null;
}
 
private final Tuple2<Set<Constraint>, Set<Index>> getConstraintsAndIndexes() throws SQLException {
final Set<Constraint> thisConstraints;
final Set<Index> thisIndexes;
if (this.getServer().getSQLSystem() != SQLSystem.MSSQL) {
thisConstraints = this.getConstraints();
thisIndexes = new HashSet<Index>(this.getIndexes(true));
} else {
thisConstraints = new HashSet<Constraint>(this.getConstraints());
thisIndexes = new HashSet<Index>();
for (final Index i : this.getIndexes()) {
final Value<String> where = i.getMSUniqueWhere();
if (!where.hasValue()) {
// regular index
thisIndexes.add(i);
} else if (where.getValue() == null) {
final Map<String, Object> map = new HashMap<String, Object>();
map.put("CONSTRAINT_NAME", i.getName());
map.put("CONSTRAINT_TYPE", "UNIQUE");
map.put("COLUMN_NAMES", i.getCols());
map.put("DEFINITION", null);
thisConstraints.add(new Constraint(this, map));
} else {
// remove extra IS NOT NULL, but does *not* translate [ARCHIVE]=(0) into
// "ARCHIVE" = 0
thisIndexes.add(this.createUniqueIndex(i.getName(), i.getCols(), where.getValue()));
}
}
}
return Tuple2.create(thisConstraints, thisIndexes);
}
 
private final Rule getRule(Rule r, SQLSystem thisSystem, SQLSystem otherSystem) {
// compare exactly
if (otherSystem == null)
1572,20 → 1699,31
}
// indexes
try {
final IPredicate<Index> pred = system.autoCreatesFKIndex() ? new IPredicate<Index>() {
@Override
public boolean evaluateChecked(Index i) {
// if auto create index, do not output current one, as it would be redundant
// (plus its name could clash with the automatic one)
return !getForeignKeysFields().contains(i.getFields());
// MS unique constraint are not standard so we're forced to create indexes "where col is
// not null" in addUniqueConstraint(). Thus when converting to another system we must
// parse indexes to recreate actual constraints.
final boolean convertMSIndex = this.getServer().getSQLSystem() == SQLSystem.MSSQL && system != SQLSystem.MSSQL;
final Set<List<SQLField>> foreignKeysFields = getForeignKeysFields();
for (final Index i : this.getIndexes(true)) {
Value<String> msWhere = null;
if (convertMSIndex && (msWhere = i.getMSUniqueWhere()).hasValue()) {
if (msWhere.getValue() != null)
Log.get().warning("MS filter might not be valid in " + system + " : " + msWhere.getValue());
res.addUniqueConstraint(i.getName(), i.getCols(), msWhere.getValue());
} else if (!system.autoCreatesFKIndex() || !foreignKeysFields.contains(i.getFields())) {
// partial unique index sometimes cannot be handled natively by the DB system
if (i.isUnique() && i.getFilter() != null && !system.isIndexFilterConditionSupported())
res.addUniqueConstraint(i.getName(), i.getCols(), i.getFilter());
else
res.addOutsideClause(syntax.getCreateIndex(i));
}
} : null;
for (final ChangeTable.OutsideClause c : syntax.getCreateIndexes(this, pred))
res.addOutsideClause(c);
}
} catch (SQLException e) {
// MAYBE fetch indexes with the rest to avoid exn now
throw new IllegalStateException("could not get indexes", e);
}
// TODO triggers, but they are system dependent and we would have to parse the SQL
// definitions to replace the different root/table name in DeferredClause.asString()
if (this.getComment() != null)
res.addOutsideClause(syntax.getSetTableComment(getComment()));
return res;
1614,12 → 1752,19
* @return the indexes mapped by column names.
* @throws SQLException if an error occurs.
*/
public final CollectionMap<String, Index> getIndexesByField() throws SQLException {
public final SetMap<String, Index> getIndexesByField() throws SQLException {
final List<Index> indexes = this.getIndexes();
final CollectionMap<String, Index> res = new CollectionMap<String, Index>(new HashSet<Index>(4), indexes.size());
final SetMap<String, Index> res = new SetMap<String, Index>(indexes.size()) {
@Override
public Set<Index> createCollection(Collection<? extends Index> v) {
final HashSet<Index> res = new HashSet<Index>(4);
res.addAll(v);
return res;
}
};
for (final Index i : indexes)
for (final String col : i.getCols())
res.put(col, i);
res.add(col, i);
return res;
}
 
1647,6 → 1792,10
* @throws SQLException if an error occurs.
*/
public synchronized final List<Index> getIndexes() throws SQLException {
return this.getIndexes(false);
}
 
protected synchronized final List<Index> getIndexes(final boolean normalized) throws SQLException {
// in pg, a unique constraint creates a unique index that is not removeable
// (except of course if we drop the constraint)
// in mysql unique constraints and indexes are one and the same thing
1680,6 → 1829,10
if (canAdd(currentIndex, uniqConstraints))
indexes.add(currentIndex);
 
if (normalized) {
indexes.addAll(this.getPartialUniqueIndexes());
}
 
// MAYBE another request to find out index.getMethod() (eg pg.getIndexesReq())
return indexes;
}
1691,56 → 1844,88
return !currentIndex.isUnique() || !uniqConstraints.contains(currentIndex.getCols());
}
 
public final class Index {
// MAYBE inline
protected synchronized final List<Index> getPartialUniqueIndexes() throws SQLException {
final SQLSystem thisSystem = this.getServer().getSQLSystem();
final List<Index> indexes = new ArrayList<Index>();
// parse triggers, TODO remove them from triggers to output in getCreateTable()
if (thisSystem == SQLSystem.H2) {
for (final Trigger t : this.triggers.values()) {
final Matcher matcher = ChangeTable.H2_UNIQUE_TRIGGER_PATTERN.matcher(t.getSQL());
if (matcher.find()) {
final String indexName = ChangeTable.getIndexName(t.getName(), thisSystem);
final String[] javaCols = ChangeTable.H2_LIST_PATTERN.split(matcher.group(1).trim());
final List<String> cols = new ArrayList<String>(javaCols.length);
for (final String javaCol : javaCols) {
cols.add(StringUtils.unDoubleQuote(javaCol));
}
final String where = StringUtils.unDoubleQuote(matcher.group(2).trim());
indexes.add(createUniqueIndex(indexName, cols, where));
}
}
} else if (thisSystem == SQLSystem.MYSQL) {
for (final Trigger t : this.triggers.values()) {
if (t.getAction().contains(ChangeTable.MYSQL_TRIGGER_EXCEPTION)) {
final String indexName = ChangeTable.getIndexName(t.getName(), thisSystem);
// MySQL needs a pair of triggers
final Trigger t2 = indexName == null ? null : this.triggers.get(indexName + ChangeTable.MYSQL_TRIGGER_SUFFIX_2);
// and their body must match
if (t2 != null && t2.getAction().equals(t.getAction())) {
final Matcher matcher = ChangeTable.MYSQL_UNIQUE_TRIGGER_PATTERN.matcher(t.getAction());
if (!matcher.find())
throw new IllegalStateException("Couldn't parse " + t.getAction());
// parse table name
final SQLName parsedName = SQLName.parse(matcher.group(1).trim());
if (!this.getName().equals(parsedName.getName()))
throw new IllegalStateException("Name mismatch : " + this.getSQLName() + " != " + parsedName);
 
final String[] wheres = ChangeTable.MYSQL_WHERE_PATTERN.split(matcher.group(2).trim());
final String userWhere = wheres[0];
 
final List<String> cols = new ArrayList<String>(wheres.length - 1);
for (int i = 1; i < wheres.length; i++) {
final Matcher eqMatcher = ChangeTable.MYSQL_WHERE_EQ_PATTERN.matcher(wheres[i].trim());
if (!eqMatcher.matches())
throw new IllegalStateException("Invalid where clause " + wheres[i]);
cols.add(SQLName.parse(eqMatcher.group(2).trim()).getName());
}
if (cols.isEmpty())
throw new IllegalStateException("No columns in " + Arrays.asList(wheres));
indexes.add(createUniqueIndex(indexName, cols, userWhere));
}
}
}
}
return indexes;
}
 
public static class SQLIndex {
 
private static final Pattern NORMALIZE_SPACES = Pattern.compile("\\s+");
 
private final String name;
// SQL, e.g. : lower("name"), "age"
private final List<String> attrs;
private final List<String> cols;
private final boolean unique;
private String method;
private String filter;
private final String filter;
 
Index(final Map<String, Object> row) {
this((String) row.get("INDEX_NAME"), (String) row.get("COLUMN_NAME"), (Boolean) row.get("NON_UNIQUE"), (String) row.get("FILTER_CONDITION"));
public SQLIndex(final String name, final List<String> attributes, final boolean unique, final String filter) {
this(name, attributes, false, unique, filter);
}
 
Index(final String name, String col, Boolean nonUnique, String filter) {
public SQLIndex(final String name, final List<String> attributes, final boolean quoteAll, final boolean unique, final String filter) {
super();
this.name = name;
this.attrs = new ArrayList<String>();
this.cols = new ArrayList<String>();
this.unique = !nonUnique;
this.attrs = new ArrayList<String>(attributes.size());
for (final String attr : attributes)
this.addAttr(quoteAll ? SQLBase.quoteIdentifier(attr) : attr);
this.unique = unique;
this.method = null;
this.filter = filter;
 
this.add(this.name, col, this.unique);
// helps when comparing
this.filter = filter == null ? null : NORMALIZE_SPACES.matcher(filter.trim()).replaceAll(" ");
}
 
public final SQLTable getTable() {
return SQLTable.this;
}
 
/**
* Adds a column to this multi-field index.
*
* @param name the name of the index.
* @param col the column to add.
* @param unique whether the index is unique.
* @throws IllegalStateException if <code>name</code> and <code>unique</code> are not the
* same as these.
*/
final void add(final String name, String col, boolean unique) {
if (!name.equals(this.name) || this.unique != unique)
throw new IllegalStateException("incoherence");
this.attrs.add(col);
if (getTable().contains(col))
this.cols.add(col);
}
 
final void add(final Index o) {
this.add(o.getName(), o.cols.get(0), o.unique);
}
 
public final String getName() {
return this.name;
}
1755,26 → 1940,13
* @return the components of this index, eg ["lower(name)", "age"].
*/
public final List<String> getAttrs() {
return this.attrs;
return Collections.unmodifiableList(this.attrs);
}
 
/**
* The table columns in this index. Note that due to db system limitation this list is
* incomplete (eg missing name).
*
* @return the columns, eg ["age"].
*/
public final List<String> getCols() {
return this.cols;
protected final void addAttr(final String attr) {
this.attrs.add(attr);
}
 
public final List<SQLField> getFields() {
final List<SQLField> res = new ArrayList<SQLField>(this.getCols().size());
for (final String f : this.getCols())
res.add(getTable().getField(f));
return res;
}
 
public final void setMethod(String method) {
this.method = method;
}
1792,24 → 1964,22
return this.filter;
}
 
final boolean isPKIndex() {
return this.isUnique() && this.getAttrs().equals(getPKsNames());
}
 
@Override
public String toString() {
return getClass().getSimpleName() + " " + this.getName() + " unique: " + this.isUnique() + " cols: " + this.getAttrs();
return getClass().getSimpleName() + " " + this.getName() + " unique: " + this.isUnique() + " cols: " + this.getAttrs() + " filter: " + this.getFilter();
}
 
// ATTN don't use name since it is often auto-generated (eg by a UNIQUE field)
@Override
public boolean equals(Object obj) {
if (obj instanceof Index) {
final Index o = (Index) obj;
return this.isUnique() == o.isUnique() && this.getAttrs().equals(o.getAttrs());
} else
if (obj instanceof SQLIndex) {
final SQLIndex o = (SQLIndex) obj;
return this.isUnique() == o.isUnique() && this.getAttrs().equals(o.getAttrs()) && CompareUtils.equals(this.getFilter(), o.getFilter())
&& CompareUtils.equals(this.getMethod(), o.getMethod());
} else {
return false;
}
}
 
// ATTN use cols, so use only after cols are done
@Override
1817,4 → 1987,122
return this.getAttrs().hashCode() + ((Boolean) this.isUnique()).hashCode();
}
}
 
private final Index createUniqueIndex(final String name, final List<String> cols, final String where) {
final Index res = new Index(name, cols.get(0), false, where);
for (int i = 1; i < cols.size(); i++) {
res.addFromMD(cols.get(i));
}
return res;
}
 
private final String removeParens(String filter) {
if (filter != null) {
filter = filter.trim();
final SQLSystem sys = this.getServer().getSQLSystem();
// postgreSQL always wrap filter with parens, ATTN we shouldn't remove from
// "(A) and (B)" but still support "(A = (0))"
if ((sys == SQLSystem.POSTGRESQL || sys == SQLSystem.MSSQL) && filter.startsWith("(") && filter.endsWith(")")) {
filter = filter.substring(1, filter.length() - 1);
}
}
return filter;
}
 
public final class Index extends SQLIndex {
 
private final List<String> cols;
 
Index(final Map<String, Object> row) {
this((String) row.get("INDEX_NAME"), (String) row.get("COLUMN_NAME"), (Boolean) row.get("NON_UNIQUE"), (String) row.get("FILTER_CONDITION"));
}
 
Index(final String name, String col, Boolean nonUnique, String filter) {
super(name, Collections.<String> emptyList(), !nonUnique, removeParens(filter));
this.cols = new ArrayList<String>();
this.addFromMD(col);
}
 
public final SQLTable getTable() {
return SQLTable.this;
}
 
/**
* The table columns in this index. Note that due to DB system limitation this list is
* incomplete (e.g. missing expressions).
*
* @return the unquoted columns, e.g. ["age"].
*/
public final List<String> getCols() {
return this.cols;
}
 
public final List<SQLField> getFields() {
final List<SQLField> res = new ArrayList<SQLField>(this.getCols().size());
for (final String f : this.getCols())
res.add(getTable().getField(f));
return res;
}
 
/**
* Adds a column to this multi-field index.
*
* @param name the name of the index.
* @param col the column to add.
* @param unique whether the index is unique.
* @throws IllegalStateException if <code>name</code> and <code>unique</code> are not the
* same as these.
*/
private final void add(final Index o) {
assert o.getAttrs().size() == 1;
if (!o.getName().equals(this.getName()) || this.isUnique() != o.isUnique())
throw new IllegalStateException("incoherence");
this.cols.addAll(o.getCols());
this.addAttr(o.getAttrs().get(0));
}
 
// col is either an expression or a column name
protected void addFromMD(String col) {
if (getTable().contains(col)) {
// e.g. age
this.cols.add(col);
this.addAttr(SQLBase.quoteIdentifier(col));
} else {
// e.g. lower("name")
this.addAttr(col);
}
}
 
final boolean isPKIndex() {
return this.isUnique() && this.getCols().equals(getTable().getPKsNames()) && this.getCols().size() == this.getAttrs().size();
}
 
private final Pattern getColPattern(final String col) {
// e.g. ([NOM] IS NOT NULL AND [PRENOM] IS NOT NULL AND [ARCHIVE]=(0))
return Pattern.compile("(?i:\\s+AND\\s+)?" + Pattern.quote(new SQLName(col).quoteMS()) + "\\s+(?i)IS\\s+NOT\\s+NULL(\\s+AND\\s+)?");
}
 
// in MS SQL we're forced to add IS NOT NULL to get the standard behaviour
// return none if it's not a unique index, otherwise the value of the where for the partial
// index (can be null)
final Value<String> getMSUniqueWhere() {
assert getServer().getSQLSystem() == SQLSystem.MSSQL;
if (this.isUnique() && this.getFilter() != null) {
String filter = this.getFilter().trim();
// for each column, remove its NOT NULL clause
for (final String col : getCols()) {
final Matcher matcher = this.getColPattern(col).matcher(filter);
if (matcher.find()) {
filter = matcher.replaceFirst("").trim();
} else {
return Value.getNone();
}
}
// what is the left is the actual filter
filter = filter.trim();
return Value.getSome(filter.isEmpty() ? null : filter);
}
return Value.getNone();
}
}
}
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLRowValuesCluster.java
19,6 → 19,7
import org.openconcerto.sql.model.graph.Link.Direction;
import org.openconcerto.sql.model.graph.Path;
import org.openconcerto.sql.utils.SQLUtils;
import org.openconcerto.utils.CollectionMap2.Mode;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.Matrix;
38,9 → 39,9
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
 
78,8 → 79,6
*/
private final List<Link> links;
private final IdentitySet<SQLRowValues> items;
// only used in store(), MAYBE returned from store() and remove getRow()
private final Map<SQLRowValues, Node> nodes;
// { vals -> listener on vals' graph }
private Map<SQLRowValues, List<ValueChangeListener>> listeners;
 
87,13 → 86,21
this.links = new ArrayList<Link>();
// SQLRowValues equals() depends on their values, but we must tell apart each reference
this.items = new IdentityHashSet<SQLRowValues>();
this.nodes = new IdentityHashMap<SQLRowValues, Node>();
this.listeners = null;
}
 
SQLRowValuesCluster(SQLRowValues vals) {
this();
addVals(-1, vals);
}
 
// add a lonely node to this
private final void addVals(final int index, final SQLRowValues vals) {
assert vals.getGraph(false) == null;
if (index < 0)
this.links.add(new Link(vals));
else
this.links.add(index, new Link(vals));
this.items.add(vals);
}
 
101,10 → 108,6
return this.links.get(0).getSrc();
}
 
private final Map<SQLRowValues, Node> getNodes() {
return this.nodes;
}
 
private final DBSystemRoot getSystemRoot() {
return this.getHead().getTable().getDBSystemRoot();
}
131,6 → 134,13
throw new IllegalArgumentException(vals + " not in " + this);
}
 
public final Set<SQLTable> getTables() {
final Set<SQLTable> res = new HashSet<SQLTable>();
for (final SQLRowValues v : this.items)
res.add(v.getTable());
return res;
}
 
void remove(SQLRowValues src, SQLField f, SQLRowValues dest) {
assert dest != null;
assert src.getGraph() == this;
168,8 → 178,8
newCluster.getListeners().put(key, this.listeners.remove(key));
}
}
assert !newCluster.items.isEmpty() && !CollectionUtils.containsAny(this.items, newCluster.items);
this.nodes.keySet().retainAll(reachable);
assert !this.items.isEmpty() && !newCluster.items.isEmpty() && !CollectionUtils.containsAny(this.items, newCluster.items) : "Empty or shared items while removing " + f + " -> " + dest
+ " from " + src;
 
for (final SQLRowValues vals : newCluster.getItems())
vals.setGraph(newCluster);
178,40 → 188,66
 
void add(SQLRowValues src, SQLField f, SQLRowValues dest) {
assert dest != null;
assert src.getGraph() == this;
assert this.contains(src);
assert src.getTable() == f.getTable();
final boolean containsSrc = this.contains(src);
final boolean containsDest = this.contains(dest);
if (!containsSrc && !containsDest)
throw new IllegalArgumentException("Neither source nor destination are contained in this :\n" + src + "\n" + dest);
 
final Link toAdd = new Link(src, f, dest);
if (this.contains(dest)) {
if (containsSrc && containsDest) {
// both source and dest are in us
this.links.add(toAdd);
} else {
assert src.getGraph() != dest.getGraph();
final SQLRowValuesCluster destGraph = dest.getGraph();
assert src.getGraph(false) != dest.getGraph(false);
final SQLRowValues rowToAdd;
final int index;
if (containsSrc) {
rowToAdd = dest;
// merge the two graphs
// add dest before since it will be needed to store us
this.links.add(0, toAdd);
this.links.addAll(0, destGraph.links);
destGraph.links.clear();
this.items.addAll(destGraph.getItems());
for (final SQLRowValues newlyAdded : destGraph.getItems()) {
// add dest after any other link from us, to keep the order of foreigns (needed for
// deepCopy())
final int srcIndex = this.links.indexOf(new Link(src));
if (srcIndex < 0)
throw new IllegalStateException("Source link not found for " + src);
index = srcIndex;
} else {
assert containsDest;
rowToAdd = src;
index = -1;
}
final SQLRowValuesCluster graphToAdd = rowToAdd.getGraph(false);
 
if (index >= 0)
this.links.add(index, toAdd);
// to preserve memory a single node has no graph unless required
// this way rowToAdd never had to create a Cluster, it will use us.
if (graphToAdd == null) {
this.addVals(index, rowToAdd);
rowToAdd.setGraph(this);
} else {
if (index < 0)
this.links.addAll(graphToAdd.links);
else
this.links.addAll(index, graphToAdd.links);
graphToAdd.links.clear();
this.items.addAll(graphToAdd.items);
for (final SQLRowValues newlyAdded : graphToAdd.items) {
newlyAdded.setGraph(this);
}
destGraph.items.clear();
if (destGraph.listeners != null) {
this.getListeners().putAll(destGraph.listeners);
destGraph.listeners = null;
graphToAdd.items.clear();
if (graphToAdd.listeners != null) {
this.getListeners().putAll(graphToAdd.listeners);
graphToAdd.listeners = null;
}
}
if (index < 0)
this.links.add(toAdd);
}
assert src.getGraph() == dest.getGraph();
}
 
public final SQLRow getRow(SQLRowValues vals) {
this.containsCheck(vals);
return this.nodes.get(vals).getStoredRow();
}
 
private IdentitySet<SQLRowValues> getReachable(final SQLRowValues from) {
final IdentitySet<SQLRowValues> res = new IdentityHashSet<SQLRowValues>();
getReachableRec(from, res);
230,18 → 266,18
}
}
 
synchronized final SQLRowValues deepCopy(SQLRowValues v) {
final SQLRowValues deepCopy(SQLRowValues v) {
// copy all rowValues of this graph
final Map<SQLRowValues, SQLRowValues> noLinkCopy = new IdentityHashMap<SQLRowValues, SQLRowValues>();
for (final SQLRowValues n : this.getItems())
noLinkCopy.put(n, new SQLRowValues(n, ForeignCopyMode.NO_COPY));
 
// and link them together
for (final SQLRowValues n : this.getItems()) {
// use referents instead of foreigns to copy order
for (final Entry<SQLField, Set<SQLRowValues>> e : n.getReferents().entrySet())
for (final SQLRowValues ref : e.getValue()) {
noLinkCopy.get(ref).put(e.getKey().getName(), noLinkCopy.get(n));
// and link them together in order
for (final Link l : this.links) {
if (l.getField() != null) {
noLinkCopy.get(l.getSrc()).put(l.getField().getName(), noLinkCopy.get(l.getDest()));
} else {
assert noLinkCopy.containsKey(l.getSrc());
}
}
 
248,17 → 284,28
return noLinkCopy.get(v);
}
 
public synchronized final void store(final StoreMode mode) throws SQLException {
this.store(mode, true);
public final StoreResult insert() throws SQLException {
return this.insert(false, false);
}
 
public final StoreResult insert(boolean insertPK, boolean insertOrder) throws SQLException {
return this.store(new Insert(insertPK, insertOrder));
}
 
public final StoreResult store(final StoreMode mode) throws SQLException {
return this.store(mode, true);
}
 
// checkValidity false useful when we want to avoid loading the graph
public synchronized final void store(final StoreMode mode, final boolean checkValidity) throws SQLException {
this.reset();
public final StoreResult store(final StoreMode mode, final boolean checkValidity) throws SQLException {
final Map<SQLRowValues, Node> nodes = new IdentityHashMap<SQLRowValues, Node>(this.size());
for (final SQLRowValues vals : this.getItems()) {
nodes.put(vals, new Node(vals));
}
// check validity first, avoid beginning a transaction for nothing
// do it after reset otherwise check previous values
if (checkValidity)
for (final Node n : this.getNodes().values()) {
for (final Node n : nodes.values()) {
n.noLink.checkValidity();
}
// this will hold the links and their ID as they are known
304,7 → 351,7
final StoringLink toStore = storingLinks.remove(0);
if (!toStore.canStore())
throw new IllegalStateException();
final Node n = getNodes().get(toStore.getSrc());
final Node n = nodes.get(toStore.getSrc());
 
// merge the maximum of links starting from the row to be stored
boolean lastDBAccess = true;
314,6 → 361,9
if (sl.getSrc() == toStore.getSrc()) {
if (sl.canStore()) {
iter.remove();
// sl can either be the main row or one the link from the row
// (bear in mind that toStore can be not the main row if the link
// destination has already been inserted)
if (sl.destID != null)
n.noLink.put(sl.getField().getName(), sl.destID);
} else {
333,7 → 383,7
for (final StoringLink sl : storingLinks) {
if (sl.getDest() == toStore.getSrc()) {
sl.destID = r.getIDNumber();
getNodes().get(sl.getSrc()).noLink.put(sl.getField().getName(), r.getIDNumber());
nodes.get(sl.getSrc()).noLink.put(sl.getField().getName(), r.getIDNumber());
}
}
}
343,7 → 393,7
// wait for the last DB access
if (lastDBAccess)
for (final Map.Entry<String, SQLRowValues> e : toStore.getSrc().getForeigns().entrySet()) {
final SQLRowValues foreign = getNodes().get(e.getValue()).getStoredValues();
final SQLRowValues foreign = nodes.get(e.getValue()).getStoredValues();
assert foreign != null : "since this the last db access for this row, all foreigns should have been inserted";
// check coherence
if (n.getStoredValues().getLong(e.getKey()) != foreign.getIDNumber().longValue())
360,15 → 410,70
// affected
n.getTable().fire(n);
}
 
return new StoreResult(nodes);
}
 
private final void reset() {
this.getNodes().clear();
for (final SQLRowValues vals : this.getItems()) {
this.getNodes().put(vals, new Node(vals));
static public final class WalkOptions {
private final Direction direction;
private RecursionType recType;
private boolean allowCycle;
private boolean includeStart;
private boolean ignoreForeignsOrder;
 
public WalkOptions(final Direction dir) {
if (dir == null)
throw new NullPointerException("No direction");
this.direction = dir;
this.recType = RecursionType.BREADTH_FIRST;
this.allowCycle = false;
this.includeStart = true;
this.ignoreForeignsOrder = true;
}
 
public Direction getDirection() {
return this.direction;
}
 
public RecursionType getRecursionType() {
return this.recType;
}
 
public WalkOptions setRecursionType(RecursionType recType) {
if (recType == null)
throw new NullPointerException("No type");
this.recType = recType;
return this;
}
 
public boolean isCycleAllowed() {
return this.allowCycle;
}
 
public WalkOptions setCycleAllowed(boolean allowCycle) {
this.allowCycle = allowCycle;
return this;
}
 
public boolean isStartIncluded() {
return this.includeStart;
}
 
public WalkOptions setStartIncluded(boolean includeStart) {
this.includeStart = includeStart;
return this;
}
 
public boolean isForeignsOrderIgnored() {
return this.ignoreForeignsOrder;
}
 
public WalkOptions setForeignsOrderIgnored(boolean ignoreForeignsOrder) {
this.ignoreForeignsOrder = ignoreForeignsOrder;
return this;
}
}
 
/**
* Walk the graph from the passed node, executing the closure for each node on the path. NOTE
* that this method only goes one way through foreign keys, ie if this cluster is a tree and
401,12 → 506,12
}
 
public final <T> StopRecurseException walk(final SQLRowValues start, T acc, ITransformer<State<T>, T> closure, RecursionType recType, final Direction foreign) {
return this.walk(start, acc, closure, recType, foreign, true);
return this.walk(start, acc, closure, new WalkOptions(foreign).setRecursionType(recType));
}
 
final <T> StopRecurseException walk(final SQLRowValues start, T acc, ITransformer<State<T>, T> closure, RecursionType recType, final Direction foreign, final boolean includeStart) {
public final <T> StopRecurseException walk(final SQLRowValues start, T acc, ITransformer<State<T>, T> closure, final WalkOptions options) {
this.containsCheck(start);
return this.walk(new State<T>(Collections.singletonList(start), Path.get(start.getTable()), acc, closure), recType, foreign, includeStart);
return this.walk(new State<T>(Collections.singletonList(start), Path.get(start.getTable()), acc, closure), options, options.isStartIncluded());
}
 
/**
414,14 → 519,13
*
* @param <T> type of acc.
* @param state the current position in the graph.
* @param recType how to recurse.
* @param direction how to cross foreign keys.
* @param options how to walk the graph.
* @param computeThisState <code>false</code> if the <code>state</code> should not be
* {@link State#compute() computed}.
* @return the exception that stopped the recursion, <code>null</code> if none was thrown.
*/
private final <T> StopRecurseException walk(final State<T> state, RecursionType recType, final Direction direction, final boolean computeThisState) {
if (computeThisState && recType == RecursionType.BREADTH_FIRST) {
private final <T> StopRecurseException walk(final State<T> state, final WalkOptions options, final boolean computeThisState) {
if (computeThisState && options.getRecursionType() == RecursionType.BREADTH_FIRST) {
final StopRecurseException e = state.compute();
if (e != null)
return e;
428,18 → 532,18
}
// get the foreign or referents rowValues
StopRecurseException res = null;
if (direction != Direction.REFERENT) {
res = rec(state, recType, direction, Direction.FOREIGN);
if (options.getDirection() != Direction.REFERENT) {
res = rec(state, options, Direction.FOREIGN);
}
if (res != null)
return res;
if (direction != Direction.FOREIGN) {
res = rec(state, recType, direction, Direction.REFERENT);
if (options.getDirection() != Direction.FOREIGN) {
res = rec(state, options, Direction.REFERENT);
}
if (res != null)
return res;
 
if (computeThisState && recType == RecursionType.DEPTH_FIRST) {
if (computeThisState && options.getRecursionType() == RecursionType.DEPTH_FIRST) {
final StopRecurseException e = state.compute();
if (e != null)
return e;
447,29 → 551,30
return null;
}
 
private <T> StopRecurseException rec(final State<T> state, RecursionType recType, final Direction direction, final Direction actualDirection) {
private <T> StopRecurseException rec(final State<T> state, final WalkOptions options, final Direction actualDirection) {
final SQLRowValues current = state.getCurrent();
final List<SQLRowValues> currentValsPath = state.getValsPath();
final SetMap<SQLField, SQLRowValues> nextVals;
if (actualDirection == Direction.FOREIGN) {
final Map<SQLField, SQLRowValues> foreigns = current.getForeignsBySQLField();
nextVals = new SetMap<SQLField, SQLRowValues>(foreigns.size());
nextVals = new SetMap<SQLField, SQLRowValues>(new LinkedHashMap<SQLField, Set<SQLRowValues>>(foreigns.size()), Mode.NULL_FORBIDDEN);
nextVals.mergeScalarMap(foreigns);
} else {
assert actualDirection == Direction.REFERENT;
nextVals = current.getReferents();
}
// predictable and repeatable order
// predictable and repeatable order (SQLRowValues.referents has no order, but .foreigns has)
final List<SQLField> keys = new ArrayList<SQLField>(nextVals.keySet());
if (actualDirection == Direction.REFERENT || options.isForeignsOrderIgnored())
Collections.sort(keys, FIELD_COMPARATOR);
for (final SQLField f : keys) {
for (final SQLRowValues v : nextVals.getNonNull(f)) {
// avoid infinite loop (don't use equals so that we can go over several equals rows)
if (!state.identityContains(v)) {
if (options.isCycleAllowed() || !state.identityContains(v)) {
final Path path = state.getPath().add(f, actualDirection);
final List<SQLRowValues> valsPath = new ArrayList<SQLRowValues>(currentValsPath);
valsPath.add(v);
final StopRecurseException e = this.walk(new State<T>(valsPath, path, state.getAcc(), state.closure), recType, direction, true);
final StopRecurseException e = this.walk(new State<T>(valsPath, path, state.getAcc(), state.closure), options, true);
if (e != null && e.isCompletely())
return e;
}
687,7 → 792,7
return sb.toString();
}
 
final String getFirstDifference(final SQLRowValues vals, final SQLRowValues other) {
final String getFirstDifference(final SQLRowValues vals, final SQLRowValues other, final boolean useForeignsOrder) {
this.containsCheck(vals);
if (this == other.getGraph())
return null;
698,29 → 803,41
return "unequal :\n" + vals + " !=\n" + other;
if (this.size() == 1)
return null;
 
// BREADTH_FIRST no need to go deep if the first values are not equals
final WalkOptions walkOptions = new WalkOptions(Direction.ANY).setRecursionType(RecursionType.BREADTH_FIRST).setForeignsOrderIgnored(!useForeignsOrder);
 
final List<SQLRowValues> flatList = new ArrayList<SQLRowValues>();
final List<Path> paths = new ArrayList<Path>();
this.walk(vals, flatList, new ITransformer<State<List<SQLRowValues>>, List<SQLRowValues>>() {
@Override
public List<SQLRowValues> transformChecked(State<List<SQLRowValues>> input) {
input.getAcc().add(input.getCurrent());
paths.add(input.getPath());
return input.getAcc();
}
}, RecursionType.BREADTH_FIRST, Direction.ANY);
}, walkOptions);
assert flatList.size() == this.size() : "missing rows";
 
// now walk the other graph, checking that each row is equal
// (this works because walk() always goes with the same order, see #FIELD_COMPARATOR)
// (this works because walk() always goes with the same order, see #FIELD_COMPARATOR and
// WalkOptions.setForeignsOrderIgnored())
final AtomicInteger index = new AtomicInteger(0);
final StopRecurseException stop = other.getGraph().walk(other, null, new ITransformer<State<Object>, Object>() {
@Override
public Object transformChecked(State<Object> input) {
final SQLRowValues thisVals = flatList.get(index.getAndIncrement());
final Path thisPath = paths.get(index.get());
if (!thisPath.equals(input.getPath()))
throw new StopRecurseException("unequal graph at index " + index.get() + " " + thisPath + " != " + input.getPath());
 
final SQLRowValues thisVals = flatList.get(index.get());
final SQLRowValues oVals = input.getCurrent();
if (!thisVals.equalsJustThis(oVals))
throw new StopRecurseException("unequal at " + input.getPath() + " :\n" + thisVals + " !=\n" + oVals);
index.incrementAndGet();
return input.getAcc();
}
}, RecursionType.BREADTH_FIRST, Direction.ANY);
}, walkOptions);
return stop == null ? null : stop.getMessage();
}
 
915,25 → 1032,45
}
}
 
public static final class StoreResult {
private final Map<SQLRowValues, Node> nodes;
 
public StoreResult(final Map<SQLRowValues, Node> nodes) {
this.nodes = nodes;
}
 
public final int getStoredCount() {
return this.nodes.size();
}
 
public final SQLRow getStoredRow(SQLRowValues vals) {
return this.nodes.get(vals).getStoredRow();
}
 
public final SQLRowValues getStoredValues(SQLRowValues vals) {
return this.nodes.get(vals).getStoredValues();
}
}
 
private static final class Node {
 
// don't use noLink since it might contains foreigns if store() was just called
// or it might be out of sync with vals since the graph is only recreated on foreign change
/** vals without any links */
private SQLRowValues noLink;
private final SQLRowValues noLink;
private final List<SQLTableEvent> modif;
 
public Node(final SQLRowValues vals) {
private Node(final SQLRowValues vals) {
this.modif = new ArrayList<SQLTableEvent>();
this.noLink = new SQLRowValues(vals, ForeignCopyMode.NO_COPY);
}
 
public SQLTableEvent store(StoreMode mode) throws SQLException {
private SQLTableEvent store(StoreMode mode) throws SQLException {
assert !this.isStored();
return this.addEvent(mode.execOn(this.noLink));
}
 
public SQLTableEvent update() throws SQLException {
private SQLTableEvent update() throws SQLException {
assert this.isStored();
 
// fields that have been updated since last store
/trunk/OpenConcerto/src/org/openconcerto/sql/model/PGSQLBase.java
20,8 → 20,8
 
public class PGSQLBase extends SQLBase {
 
PGSQLBase(SQLServer server, String name, String login, String pass, IClosure<SQLDataSource> dsInit) {
super(server, name, login, pass, dsInit);
PGSQLBase(SQLServer server, String name, IClosure<? super DBSystemRoot> systemRootInit, String login, String pass, IClosure<? super SQLDataSource> dsInit) {
super(server, name, systemRootInit, login, pass, dsInit);
}
 
// *** quoting
32,6 → 32,8
@Override
public final String quoteString(String s) {
final String res = super.quoteString(s);
if (s == null)
return res;
// see PostgreSQL Documentation 4.1.2.1 String Constants
// escape \ by replacing them with \\
final Matcher matcher = BACKSLASH_PATTERN.matcher(res);
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLSyntaxPG.java
15,11 → 15,13
 
import org.openconcerto.sql.changer.correct.FixSerial;
import org.openconcerto.sql.model.SQLField.Properties;
import org.openconcerto.sql.model.SQLTable.Index;
import org.openconcerto.sql.model.SQLTable.SQLIndex;
import org.openconcerto.sql.model.graph.TablesMap;
import org.openconcerto.sql.utils.ChangeTable.ClauseType;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.FileUtils;
import org.openconcerto.utils.ListMap;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.utils.cc.IClosure;
import org.openconcerto.utils.cc.ITransformer;
42,7 → 44,6
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;
 
65,27 → 66,36
*/
class SQLSyntaxPG extends SQLSyntax {
 
// From http://www.postgresql.org/docs/9.0/interactive/multibyte.html
static final short MAX_BYTES_PER_CHAR = 4;
// http://www.postgresql.org/docs/9.0/interactive/datatype-character.html
private static final short MAX_LENGTH_BYTES = 4;
// https://wiki.postgresql.org/wiki/FAQ#What_is_the_maximum_size_for_a_row.2C_a_table.2C_and_a_database.3F
private static final int MAX_FIELD_SIZE = 1024 * 1024 * 1024;
 
private static final int MAX_VARCHAR_L = (MAX_FIELD_SIZE - MAX_LENGTH_BYTES) / MAX_BYTES_PER_CHAR;
 
SQLSyntaxPG() {
super(SQLSystem.POSTGRESQL);
this.typeNames.putAll(Boolean.class, "boolean", "bool", "bit");
this.typeNames.putAll(Short.class, "smallint");
this.typeNames.putAll(Integer.class, "integer", "int", "int4");
this.typeNames.putAll(Long.class, "bigint", "int8");
this.typeNames.putAll(BigDecimal.class, "decimal", "numeric");
this.typeNames.putAll(Float.class, "real", "float4");
this.typeNames.putAll(Double.class, "double precision", "float8");
this.typeNames.addAll(Boolean.class, "boolean", "bool", "bit");
this.typeNames.addAll(Short.class, "smallint");
this.typeNames.addAll(Integer.class, "integer", "int", "int4");
this.typeNames.addAll(Long.class, "bigint", "int8");
this.typeNames.addAll(BigDecimal.class, "decimal", "numeric");
this.typeNames.addAll(Float.class, "real", "float4");
this.typeNames.addAll(Double.class, "double precision", "float8");
// since 7.3 default is without timezone
this.typeNames.putAll(Timestamp.class, "timestamp", "timestamp without time zone");
this.typeNames.putAll(java.util.Date.class, "time", "time without time zone", "date");
this.typeNames.putAll(Blob.class, "bytea");
this.typeNames.putAll(Clob.class, "varchar", "char", "character varying", "character", "text");
this.typeNames.putAll(String.class, "varchar", "char", "character varying", "character", "text");
this.typeNames.addAll(Timestamp.class, "timestamp", "timestamp without time zone");
this.typeNames.addAll(java.util.Date.class, "time", "time without time zone", "date");
this.typeNames.addAll(Blob.class, "bytea");
this.typeNames.addAll(Clob.class, "varchar", "char", "character varying", "character", "text");
this.typeNames.addAll(String.class, "varchar", "char", "character varying", "character", "text");
}
 
public String getInitRoot(final String name) {
final String sql;
try {
final String fileContent = FileUtils.read(SQLSyntaxPG.class.getResourceAsStream("pgsql-functions.sql"), "UTF8");
final String fileContent = FileUtils.readUTF8(SQLSyntaxPG.class.getResourceAsStream("pgsql-functions.sql"));
sql = fileContent.replace("${rootName}", SQLBase.quoteIdentifier(name));
} catch (IOException e) {
throw new IllegalStateException("cannot read functions", e);
111,6 → 121,11
return " serial";
}
 
@Override
public int getMaximumVarCharLength() {
return MAX_VARCHAR_L;
}
 
private String changeFKChecks(DBRoot r, final String action) {
String res = r.getBase().quote("select %i.getTables(%s, '.*', 'tables_changeFKChecks');", r.getName(), r.getName());
res += r.getBase().quote("select %i.setTrigger('" + action + "', 'tables_changeFKChecks');", r.getName());
161,7 → 176,7
//
+ "WHERE ci.relkind IN ('i','') AND n.nspname <> 'pg_catalog' AND n.nspname !~ '^pg_toast'\n"
//
+ " AND n.nspname = '" + t.getSchema().getName() + "' AND ct.relname ~ '^(" + t.getName() + ")$'\n"
+ " AND n.nspname = " + t.getBase().quoteString(t.getSchema().getName()) + " AND ct.relname = " + t.getBase().quoteString(t.getName()) + "\n"
//
+ "ORDER BY \"NON_UNIQUE\", \"TYPE\", \"INDEX_NAME\", \"ORDINAL_POSITION\";";
// don't cache since we don't listen on system tables
173,7 → 188,7
}
 
@Override
public List<String> getAlterField(SQLField f, Set<Properties> toAlter, String type, String defaultVal, Boolean nullable) {
public Map<ClauseType, List<String>> getAlterField(SQLField f, Set<Properties> toAlter, String type, String defaultVal, Boolean nullable) {
final List<String> res = new ArrayList<String>();
if (toAlter.contains(Properties.NULLABLE))
res.add(this.setNullable(f, nullable));
185,7 → 200,7
newType = getType(f);
if (toAlter.contains(Properties.DEFAULT))
res.add(this.setDefault(f, defaultVal));
return res;
return ListMap.singleton(ClauseType.ALTER_COL, res);
}
 
@Override
209,9 → 224,8
}
 
@Override
protected String getCreateIndex(final String cols, final SQLName tableName, Index i) {
protected String getCreateIndex(final String cols, final SQLName tableName, SQLIndex i) {
final String method = i.getMethod() != null ? " USING " + i.getMethod() : "";
// TODO handle where
return "ON " + tableName.quote() + " " + method + cols;
}
 
278,7 → 292,7
}
 
@Override
protected void _storeData(final SQLTable t, final File f) {
protected void _storeData(final SQLTable t, final File f) throws IOException {
// if there's no fields, there's no data
if (t.getFields().size() == 0)
return;
307,12 → 321,12
}
});
} catch (Exception e) {
throw new IllegalStateException("unable to store " + t + " into " + f, e);
throw new IOException("unable to store " + t + " into " + f, e);
}
}
 
static SQLSelect selectAll(final SQLTable t) {
final SQLSelect sel = new SQLSelect(t.getBase(), true);
final SQLSelect sel = new SQLSelect(true);
for (final SQLField field : t.getOrderedFields()) {
// MySQL despite accepting 'boolean', 'true' and 'false' keywords doesn't really
// support booleans
340,8 → 354,8
}
 
@Override
public SQLBase createBase(SQLServer server, String name, String login, String pass, IClosure<SQLDataSource> dsInit) {
return new PGSQLBase(server, name, login, pass, dsInit);
public SQLBase createBase(SQLServer server, String name, final IClosure<? super DBSystemRoot> systemRootInit, String login, String pass, IClosure<? super SQLDataSource> dsInit) {
return new PGSQLBase(server, name, systemRootInit, login, pass, dsInit);
}
 
@Override
410,7 → 424,8
@SuppressWarnings("unchecked")
public List<Map<String, Object>> getConstraints(SQLBase b, TablesMap tables) throws SQLException {
final String sel = "select nsp.nspname as \"TABLE_SCHEMA\", rel.relname as \"TABLE_NAME\", c.conname as \"CONSTRAINT_NAME\", c.oid as cid, \n"
+ "case c.contype when 'u' then 'UNIQUE' when 'c' then 'CHECK' when 'f' then 'FOREIGN KEY' when 'p' then 'PRIMARY KEY' end as \"CONSTRAINT_TYPE\", att.attname as \"COLUMN_NAME\", c.conkey as \"colsNum\", att.attnum as \"colNum\"\n"
+ "case c.contype when 'u' then 'UNIQUE' when 'c' then 'CHECK' when 'f' then 'FOREIGN KEY' when 'p' then 'PRIMARY KEY' end as \"CONSTRAINT_TYPE\", att.attname as \"COLUMN_NAME\", pg_get_constraintdef(c.oid) as \"DEFINITION\","
+ "c.conkey as \"colsNum\", att.attnum as \"colNum\"\n"
// from
+ "from pg_catalog.pg_constraint c\n" + "join pg_namespace nsp on nsp.oid = c.connamespace\n" + "left join pg_class rel on rel.oid = c.conrelid\n"
+ "left join pg_attribute att on att.attrelid = c.conrelid and att.attnum = ANY(c.conkey)\n"
486,18 → 501,4
public String getDropTrigger(Trigger t) {
return "DROP TRIGGER " + SQLBase.quoteIdentifier(t.getName()) + " on " + t.getTable().getSQLName().quote();
}
 
@Override
public String getUpdate(SQLTable t, List<String> tables, Map<String, String> setPart) {
String res = t.getSQLName().quote() + " SET\n" + CollectionUtils.join(setPart.entrySet(), ",\n", new ITransformer<Entry<String, String>, String>() {
@Override
public String transformChecked(Entry<String, String> input) {
// pg require that fields are unprefixed
return SQLBase.quoteIdentifier(input.getKey()) + " = " + input.getValue();
}
});
if (tables.size() > 0)
res += " FROM " + CollectionUtils.join(tables, ", ");
return res;
}
}
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLSelect.java
56,8 → 56,10
 
// [String], eg : [SITE.ID_SITE, AVG(AGE)]
private final List<String> select;
// [SQLField], eg : [|SITE.ID_SITE|], known fields in this select (addRawSelect)
private final List<SQLField> selectFields;
// names of columns (explicit aliases and field names), e.g. [ID_SITE, null]
private final List<String> selectNames;
// e.g. : [|SITE.ID_SITE|], known fields in this select (addRawSelect)
private final List<FieldRef> selectFields;
private Where where;
private final List<FieldRef> groupBy;
private Where having;
91,7 → 93,7
* Create a new SQLSelect.
*
* @param base the database of the request.
* @deprecated use {@link #SQLSelect(DBSystemRoot)}
* @deprecated use {@link #SQLSelect(DBSystemRoot, boolean)}
*/
public SQLSelect(SQLBase base) {
this(base, false);
127,7 → 129,8
*/
public SQLSelect(DBSystemRoot sysRoot, boolean plain) {
this.select = new ArrayList<String>();
this.selectFields = new ArrayList<SQLField>();
this.selectNames = new ArrayList<String>();
this.selectFields = new ArrayList<FieldRef>();
this.where = null;
this.groupBy = new ArrayList<FieldRef>();
this.having = null;
162,7 → 165,8
public SQLSelect(SQLSelect orig) {
// ATTN synch les implémentations des attributs (LinkedHashSet, ...)
this.select = new ArrayList<String>(orig.select);
this.selectFields = new ArrayList<SQLField>(orig.selectFields);
this.selectNames = new ArrayList<String>(orig.selectNames);
this.selectFields = new ArrayList<FieldRef>(orig.selectFields);
this.where = orig.where == null ? null : new Where(orig.where);
this.groupBy = new ArrayList<FieldRef>(orig.groupBy);
this.having = orig.having == null ? null : new Where(orig.having);
178,6 → 182,7
 
this.waitTrx = orig.waitTrx;
this.waitTrxTables = new ArrayList<String>(orig.waitTrxTables);
this.limit = orig.limit;
}
 
public final SQLSystem getSQLSystem() {
287,23 → 292,38
}
 
/**
* Fields names of the SELECT
* SQL expressions of the SELECT.
*
* @return a list of fields names used by the SELECT
* @return a list of expressions used by the SELECT, e.g. "T.*, A.f", "count(*)".
*/
public List<String> getSelect() {
return this.select;
return Collections.unmodifiableList(this.select);
}
 
/**
* Fields of the SELECT
* Column names of the SELECT. Should always have the same length and same indexes as the result
* set, i.e. will contain <code>null</code> for computed columns without aliases. But the length
* may not be equal to that of {@link #getSelect()}, e.g. when using
* {@link #addSelectStar(TableRef)} which add one expression but all the fields.
*
* @return a list of fields used by the SELECT
* @return a list of column names of the SELECT, <code>null</code> for indexes without any.
*/
public final List<SQLField> getSelectFields() {
return this.selectFields;
public List<String> getSelectNames() {
return Collections.unmodifiableList(this.selectNames);
}
 
/**
* Fields of the SELECT. Should always have the same length and same indexes as the result set,
* i.e. will contain <code>null</code> for computed columns. But the length may not be equal to
* that of {@link #getSelect()}, e.g. when using {@link #addSelectStar(TableRef)} which add one
* expression but all the fields.
*
* @return a list of fields used by the SELECT, <code>null</code> for indexes without any.
*/
public final List<FieldRef> getSelectFields() {
return Collections.unmodifiableList(this.selectFields);
}
 
public List<String> getOrder() {
return this.order;
}
472,27 → 492,39
}
 
public SQLSelect addSelect(FieldRef f, String function, String alias) {
final String defaultAlias;
String s = f.getFieldRef();
if (function != null) {
s = function + "(" + s + ")";
defaultAlias = function;
} else {
defaultAlias = f.getField().getName();
}
this.from.add(this.declaredTables.add(f));
this.selectFields.add(f.getField());
return this.addRawSelect(s, alias);
return this.addRawSelect(f, s, alias, defaultAlias);
}
 
/**
* To add an item that is not a field.
*
* @param expr any legal exp in a SELECT statement (eg a constant, a complex function, etc).
* @param expr any legal exp in a SELECT statement (e.g. a constant, a complex function, etc).
* @param alias a name for the expression, may be <code>null</code>.
* @return this.
*/
public SQLSelect addRawSelect(String expr, String alias) {
return this.addRawSelect(null, expr, alias, null);
}
 
// private since we can't check that f is used in expr
// defaultName only used if alias is null
private SQLSelect addRawSelect(FieldRef f, String expr, String alias, String defaultName) {
if (alias != null) {
expr += " as " + SQLBase.quoteIdentifier(alias);
}
this.select.add(expr);
if (f != null)
this.from.add(this.declaredTables.add(f));
this.selectFields.add(f);
this.selectNames.add(alias != null ? alias : defaultName);
return this;
}
 
509,7 → 541,10
public SQLSelect addSelectStar(TableRef table) {
this.select.add(SQLBase.quoteIdentifier(table.getAlias()) + ".*");
this.from.add(this.declaredTables.add(table));
this.selectFields.addAll(table.getTable().getOrderedFields());
final List<SQLField> allFields = table.getTable().getOrderedFields();
this.selectFields.addAll(allFields);
for (final SQLField f : allFields)
this.selectNames.add(f.getName());
return this;
}
 
1003,7 → 1038,11
* @return all fields known to this instance.
*/
public final Set<SQLField> getFields() {
final Set<SQLField> res = new HashSet<SQLField>(this.getSelectFields());
final Set<SQLField> res = new HashSet<SQLField>(this.getSelectFields().size());
for (final FieldRef f : this.getSelectFields()) {
if (f != null)
res.add(f.getField());
}
for (final SQLSelectJoin j : getJoins())
res.addAll(getFields(j.getWhere()));
res.addAll(getFields(this.getWhere()));
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLImmutableRowValues.java
89,8 → 89,8
}
 
@Override
public int getForeignID(String fieldName) {
return this.delegate.getForeignID(fieldName);
public Number getForeignIDNumber(String fieldName) throws IllegalArgumentException {
return this.delegate.getForeignIDNumber(fieldName);
}
 
public boolean isDefault(String fieldName) {
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLRowAccessor.java
34,6 → 34,79
* A class that represent a row of a table. The row might not acutally exists in the database, and
* it might not define all the fields.
*
* <table border="1">
* <caption>Primary Key</caption> <thead>
* <tr>
* <th><code>ID</code> value</th>
* <th>{@link #hasID()}</th>
* <th>{@link #getIDNumber()}</th>
* <th>{@link #isUndefined()}</th>
* </tr>
* </thead> <tbody>
* <tr>
* <th>∅</th>
* <td><code>false</code></td>
* <td><code>null</code></td>
* <td><code>false</code></td>
* </tr>
* <tr>
* <th><code>null</code></th>
* <td><code>false</code> :<br/>
* no row in the DB can have a <code>null</code> primary key</td>
* <td><code>null</code></td>
* <td><code>false</code><br/>
* (even if getUndefinedIDNumber() is <code>null</code>, see method documentation)</td>
* </tr>
* <tr>
* <th><code>instanceof Number</code></th>
* <td><code>true</code></td>
* <td><code>Number</code></td>
* <td>if equals <code>getUndefinedID()</code></td>
* </tr>
* <tr>
* <th><code>else</code></th>
* <td><code>ClassCastException</code></td>
* <td><code>ClassCastException</code></td>
* <td><code>ClassCastException</code></td>
* </tr>
* </tbody>
* </table>
* <br/>
* <table border="1">
* <caption>Foreign Keys</caption> <thead>
* <tr>
* <th><code>ID</code> value</th>
* <th>{@link #getForeignIDNumber(String)}</th>
* <th>{@link #isForeignEmpty(String)}</th>
* </tr>
* </thead> <tbody>
* <tr>
* <th>∅</th>
* <td><code>Exception</code></td>
* <td><code>Exception</code></td>
* </tr>
* <tr>
* <th><code>null</code></th>
* <td><code>null</code></td>
* <td>if equals <code>getUndefinedID()</code></td>
* </tr>
* <tr>
* <th><code>instanceof Number</code></th>
* <td><code>Number</code></td>
* <td>if equals <code>getUndefinedID()</code></td>
* </tr>
* <tr>
* <tr>
* <th><code>instanceof SQLRowValues</code></th>
* <td><code>getIDNumber()</code></td>
* <td><code>isUndefined()</code></td>
* </tr>
* <th><code>else</code></th>
* <td><code>ClassCastException</code></td>
* <td><code>ClassCastException</code></td>
* </tr> </tbody>
* </table>
*
* @author Sylvain CUAZ
*/
public abstract class SQLRowAccessor implements SQLData {
52,6 → 125,18
}
 
/**
* Whether this row has a Number for the primary key.
*
* @return <code>true</code> if the value of the primary key is specified and is a non
* <code>null</code> number, <code>false</code> if the value isn't specified or if it's
* <code>null</code>.
* @throws ClassCastException if value is not <code>null</code> and not a {@link Number}.
*/
public final boolean hasID() throws ClassCastException {
return this.getIDNumber() != null;
}
 
/**
* Returns the ID of the represented row.
*
* @return the ID, or {@link SQLRow#NONEXISTANT_ID} if this row is not linked to the DB.
60,11 → 145,38
 
public abstract Number getIDNumber();
 
/**
* Whether this row is the undefined row. Return <code>false</code> if both the
* {@link #getIDNumber() ID} and {@link SQLTable#getUndefinedIDNumber()} are <code>null</code>
* since no row can have <code>null</code> primary key in the database. IOW when
* {@link SQLTable#getUndefinedIDNumber()} is <code>null</code> the empty
* <strong>foreign</strong> keys are <code>null</code>.
*
* @return <code>true</code> if the ID is specified, not <code>null</code> and is equal to the
* {@link SQLTable#getUndefinedIDNumber() undefined} ID.
*/
public final boolean isUndefined() {
return this.getID() == this.getTable().getUndefinedID();
final Number id = this.getIDNumber();
return id != null && id.intValue() == this.getTable().getUndefinedID();
}
 
/**
* Est ce que cette ligne est archivée.
*
* @return <code>true</code> si la ligne était archivée lors de son instanciation.
*/
public boolean isArchived() {
// si il n'y a pas de champs archive, elle n'est pas archivée
if (!this.getTable().isArchivable())
return false;
// TODO sortir archive == 1
if (this.getTable().getArchiveField().getType().getJavaType().equals(Boolean.class))
return this.getBoolean(this.getTable().getArchiveField().getName()) == Boolean.TRUE;
else
return this.getInt(this.getTable().getArchiveField().getName()) == 1;
}
 
/**
* Creates an SQLRow from these values, without any DB access.
*
* @return an SQLRow with the same values as this.
193,8 → 305,29
* <code>null</code>.
* @throws IllegalArgumentException if fieldName is not a foreign field.
*/
public abstract int getForeignID(String fieldName) throws IllegalArgumentException;
public final int getForeignID(String fieldName) throws IllegalArgumentException {
final Number res = this.getForeignIDNumber(fieldName);
return res == null ? SQLRow.NONEXISTANT_ID : res.intValue();
}
 
/**
* Return the ID of a foreign row.
*
* @param fieldName name of the foreign field.
* @return the value of <code>fieldName</code> or {@link #getIDNumber()} if the value is a
* {@link SQLRowAccessor}, <code>null</code> if the actual value is.
* @throws IllegalArgumentException if fieldName is not a foreign field or if the field isn't
* specified.
*/
public abstract Number getForeignIDNumber(String fieldName) throws IllegalArgumentException;
 
/**
* Whether the passed field is empty.
*
* @param fieldName name of the foreign field.
* @return <code>true</code> if {@link #getForeignIDNumber(String)} is the
* {@link SQLTable#getUndefinedIDNumber()}.
*/
public abstract boolean isForeignEmpty(String fieldName);
 
public abstract Collection<? extends SQLRowAccessor> getReferentRows();
/trunk/OpenConcerto/src/org/openconcerto/sql/model/DBRoot.java
198,8 → 198,7
// PK from the DB, but use our order
// don't try to validate since table has neither undefined row nor
// constraints
vals.getGraph().store(new Insert(false, true), false);
final SQLRow undefRow = vals.getGraph().getRow(vals);
final SQLRow undefRow = vals.getGraph().store(new Insert(false, true), false).getStoredRow(vals);
SQLTable.setUndefID(getSchema(), tableName, undefRow.getID());
newUndefIDs.put(createTable, undefRow.getIDNumber());
}
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLSearchMode.java
New file
0,0 → 1,47
/*
* 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.model;
 
public abstract class SQLSearchMode {
 
static public final SQLSearchMode EQUALS = new SQLSearchMode() {
@Override
public String generateSQL(final DBRoot r, final String term) {
return " = " + r.getBase().quoteString(term);
}
};
 
static public final SQLSearchMode CONTAINS = new SQLSearchMode() {
@Override
public String generateSQL(final DBRoot r, final String term) {
return " like " + r.getBase().quoteString("%" + SQLSyntax.get(r).getLitteralLikePattern(term) + "%");
}
};
static public final SQLSearchMode STARTS_WITH = new SQLSearchMode() {
@Override
public String generateSQL(final DBRoot r, final String term) {
return " like " + r.getBase().quoteString(SQLSyntax.get(r).getLitteralLikePattern(term) + "%");
}
};
 
static public final SQLSearchMode ENDS_WITH = new SQLSearchMode() {
@Override
public String generateSQL(final DBRoot r, final String term) {
return " like " + r.getBase().quoteString("%" + SQLSyntax.get(r).getLitteralLikePattern(term));
}
};
 
public abstract String generateSQL(final DBRoot r, final String term);
 
}
/trunk/OpenConcerto/src/org/openconcerto/sql/model/MSSQLBase.java
New file
0,0 → 1,41
/*
* 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.model;
 
import org.openconcerto.utils.cc.IClosure;
 
public class MSSQLBase extends SQLBase {
 
MSSQLBase(SQLServer server, String name, IClosure<? super DBSystemRoot> systemRootInit, String login, String pass, IClosure<? super SQLDataSource> dsInit) {
super(server, name, systemRootInit, login, pass, dsInit);
}
 
// *** quoting
 
@Override
public final String quoteString(String s) {
final String res = super.quoteString(s);
if (s == null)
return res;
// only use escape form if needed (=> equals with other systems most of the time)
boolean simpleASCII = true;
final int l = s.length();
for (int i = 0; simpleASCII && i < l; i++) {
final char c = s.charAt(i);
simpleASCII = c <= 0xFF;
}
// see http://msdn.microsoft.com/fr-fr/library/ms191200(v=sql.105).aspx
return simpleASCII ? res : "N" + res;
}
}
/trunk/OpenConcerto/src/org/openconcerto/sql/model/SQLFieldsSet.java
16,14 → 16,17
*/
package org.openconcerto.sql.model;
 
import org.openconcerto.utils.CollectionMap;
import org.openconcerto.utils.CollectionMap2Itf.SetMapItf;
import org.openconcerto.utils.SetMap;
 
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
 
import net.jcip.annotations.Immutable;
 
/**
* Un ensemble de champs SQL. Les champs sont indexés par table, et on peut donc connaître
* l'ensemble des tables des champs ou encore avoir tous les champs pour une table.
32,19 → 35,55
* @see #getFields(SQLTable)
* @author ILM Informatique 12 mai 2004
*/
@Immutable
public class SQLFieldsSet {
static private final SQLFieldsSet EMPTY = new SQLFieldsSet(SetMap.<SQLTable, SQLField> empty(), true);
 
// SQLTable => {SQLField}
private final CollectionMap<SQLTable, SQLField> tables;
private String name;
static public SQLFieldsSet empty() {
return EMPTY;
}
 
/**
* Crée un ensemble vide.
*/
public SQLFieldsSet() {
this(new HashSet<SQLField>());
static public SQLFieldsSet create(final SQLTable t, final String... names) {
final SetMapItf<SQLTable, SQLField> res = createMap();
for (final String name : names) {
res.add(t, t.getField(name));
}
return new SQLFieldsSet(res, false);
}
 
static public SQLFieldsSet create(final Map<SQLTable, ? extends Collection<SQLField>> fields) {
final SetMapItf<SQLTable, SQLField> res = createMap();
res.merge(fields);
return new SQLFieldsSet(res, false);
}
 
static private final SetMapItf<SQLTable, SQLField> toSetMap(final Collection<SQLField> fields) {
final SetMapItf<SQLTable, SQLField> res = createMap();
for (final SQLField f : fields)
res.add(f.getTable(), f);
return res;
}
 
static private SetMapItf<SQLTable, SQLField> createMap() {
return new SetMap<SQLTable, SQLField>() {
@Override
public Set<SQLField> createCollection(Collection<? extends SQLField> v) {
final LinkedHashSet<SQLField> res = new LinkedHashSet<SQLField>(8);
res.addAll(v);
return res;
}
};
}
 
static public final Set<String> getNames(final Collection<SQLField> fields) {
final Set<String> res = new HashSet<String>(fields.size());
for (final SQLField f : fields)
res.add(f.getName());
return res;
}
 
private final SetMapItf<SQLTable, SQLField> tables;
 
/**
* Crée un ensemble composé des champs passés.
*
51,30 → 90,17
* @param fields un ensemble de SQLField, l'ensemble n'est pas modifié.
*/
public SQLFieldsSet(final Collection<SQLField> fields) {
this.tables = new CollectionMap<SQLTable, SQLField>(LinkedHashSet.class);
this.setFields(fields);
this(toSetMap(fields), false);
}
 
private void setFields(final Collection<SQLField> fields) {
this.tables.clear();
for (final SQLField field : fields) {
this.add(field);
private SQLFieldsSet(final SetMapItf<SQLTable, SQLField> fields, final boolean unmodif) {
this.tables = unmodif ? fields : SetMap.unmodifiableMap(fields);
}
}
 
/**
* Ajoute un champ.
*
* @param field le champ a ajouté.
*/
public final void add(final SQLField field) {
this.tables.put(field.getTable(), field);
public final SetMapItf<SQLTable, SQLField> getFields() {
return this.tables;
}
 
public final void retain(final SQLTable t) {
this.tables.keySet().retainAll(Collections.singleton(t));
}
 
/**
* Retourne tous les champs de cet ensemble appartenant à la table passée.
*
82,7 → 108,7
* @return l'ensemble des champs appartenant à la table.
*/
public final Set<SQLField> getFields(final SQLTable table) {
return (Set<SQLField>) this.tables.getNonNull(table);
return this.tables.getNonNull(table);
}
 
public final Set<SQLField> getFields(final String table) {
94,10 → 120,7
}
 
public final Set<String> getFieldsNames(final SQLTable table) {
final Set<String> res = new HashSet<String>();
for (final SQLField f : this.getFields(table))
res.add(f.getName());
return res;
return getNames(this.getFields(table));
}
 
/**
106,25 → 129,12
* @return l'ensemble des SQLTable.
*/
public final Set<SQLTable> getTables() {
return Collections.unmodifiableSet(this.tables.keySet());
return this.tables.keySet();
}
 
/**
* Retourne toutes les champs.
*
* @return l'ensemble des SQLField.
*/
public final Set<SQLField> asSet() {
return new HashSet<SQLField>(this.tables.values());
}
 
public final void setName(final String string) {
this.name = string;
}
 
@Override
public String toString() {
return super.toString() + " " + this.name;
return this.getClass().getSimpleName() + " " + this.tables;
}
 
}
/trunk/OpenConcerto/src/org/openconcerto/sql/utils/AlterTable.java
19,6 → 19,7
import org.openconcerto.sql.model.SQLField.Properties;
import org.openconcerto.sql.model.SQLName;
import org.openconcerto.sql.model.SQLSyntax;
import org.openconcerto.sql.model.SQLSystem;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.graph.Link;
import org.openconcerto.utils.CollectionUtils;
29,6 → 30,8
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
 
/**
72,6 → 75,8
}
 
public final AlterTable dropColumn(String name) {
if (this.getSyntax().getSystem() == SQLSystem.MSSQL)
this.alterColumnDefault(name, null);
return this.addClause("DROP COLUMN " + SQLBase.quoteIdentifier(name), ClauseType.DROP_COL);
}
 
139,8 → 144,16
* @return this.
*/
public final AlterTable alterColumn(String fname, SQLField from, Set<Properties> toTake) {
for (final String s : this.getSyntax().getAlterField(this.t.getField(fname), from, toTake))
this.addClause(s, ClauseType.ALTER_COL);
return this.addClauses(this.getSyntax().getAlterField(this.t.getField(fname), from, toTake));
}
 
private final AlterTable addClauses(final Map<ClauseType, List<String>> res) {
for (final Entry<ClauseType, List<String>> e : res.entrySet()) {
final ClauseType type = e.getKey();
for (final String s : e.getValue()) {
this.addClause(s, type);
}
}
return thisAsT();
}
 
157,9 → 170,7
* @see #alterColumn(String, SQLField, Set)
*/
public final AlterTable alterColumn(String fname, Set<Properties> toAlter, String type, String defaultVal, Boolean nullable) {
for (final String s : this.getSyntax().getAlterField(this.t.getField(fname), toAlter, type, defaultVal, nullable))
this.addClause(s, ClauseType.ALTER_COL);
return thisAsT();
return this.addClauses(this.getSyntax().getAlterField(this.t.getField(fname), toAlter, type, defaultVal, nullable));
}
 
public final AlterTable alterColumnNullable(String f, boolean b) {
190,6 → 201,10
}
 
public final AlterTable dropIndex(final String name) {
return this.dropIndex(name, true);
}
 
private final AlterTable dropIndex(final String name, final boolean exact) {
return this.addOutsideClause(new OutsideClause() {
@Override
public ClauseType getType() {
198,12 → 213,41
 
@Override
public String asString(SQLName tableName) {
return getSyntax().getDropIndex(name, tableName);
return getSyntax().getDropIndex(exact ? name : getIndexName(tableName, name), tableName);
}
});
}
 
public final AlterTable dropUniqueConstraint(final String name, final boolean partial) {
final SQLSystem system = getSyntax().getSystem();
if (system == SQLSystem.MSSQL) {
return this.dropIndex(name, false);
} else if (!partial) {
return this.addClause(new DeferredClause() {
@Override
public String asString(ChangeTable<?> ct, SQLName tableName) {
return getSyntax().getDropConstraint() + getQuotedConstraintName(tableName, name);
}
 
@Override
public ClauseType getType() {
return ClauseType.DROP_CONSTRAINT;
}
});
} else if (system == SQLSystem.POSTGRESQL) {
return this.dropIndex(name, false);
} else if (system == SQLSystem.H2) {
return this.addOutsideClause(new DropUniqueTrigger(name));
} else if (system == SQLSystem.MYSQL) {
for (final String event : TRIGGER_EVENTS)
this.addOutsideClause(new DropUniqueTrigger(name, event));
return thisAsT();
} else {
throw new UnsupportedOperationException("System isn't supported : " + system);
}
}
 
@Override
protected String asString(final NameTransformer transf, ConcatStep step) {
return this.asString(transf, step.getTypes());
}
/trunk/OpenConcerto/src/org/openconcerto/sql/utils/SQLUtils.java
24,6 → 24,7
 
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Savepoint;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
82,7 → 83,7
}
 
/**
* Use a single transaction to execute <code>h</code> : it is either committed or rollbacked.
* Use a single transaction to execute <code>h</code> : it is either committed or rolled back.
*
* @param <T> type of return
* @param <X> type of exception of <code>h</code>
89,13 → 90,42
* @param ds the datasource where h should be executed.
* @param h the code to execute.
* @return what h returns.
* @throws SQLException if a pb occurs.
* @throws SQLException if a problem occurs.
* @throws X if <code>h</code> throw it.
*/
public static <T, X extends Exception> T executeAtomic(final SQLDataSource ds, final ConnectionHandlerNoSetup<T, X> h) throws SQLException, X {
return executeAtomic(ds, h, true);
}
 
/**
* Execute <code>h</code> in a transaction. Only the outer most call to
* <code>executeAtomic()</code> commit or roll back a transaction, for recursive calls if
* <code>continueTx</code> is <code>true</code> then nothing happens, else a save point is
* created and rolled back if an exception occurs (allowing the caller to catch the exception
* without loosing the current transaction).
* <p>
* NOTE : if <code>continueTx</code> is <code>true</code> and an exception is thrown, the
* connection might be aborted. So you should notify the caller, e.g. propagate the exception so
* that he can roll back the transaction.
* </p>
*
* @param <T> type of return
* @param <X> type of exception of <code>h</code>
* @param ds the data source where h should be executed.
* @param h the code to execute.
* @param continueTx only relevant if already in a transaction : if <code>true</code> the
* handler will just be executed and the connection won't be modified (i.e. the existing
* transaction will neither be committed nor rolled back) ; if <code>false</code> a save
* point will be used.
* @return what h returns.
* @throws SQLException if a problem occurs.
* @throws X if <code>h</code> throw it.
*/
public static <T, X extends Exception> T executeAtomic(final SQLDataSource ds, final ConnectionHandlerNoSetup<T, X> h, final boolean continueTx) throws SQLException, X {
return ds.useConnection(new ConnectionHandler<T, X>() {
 
private Boolean autoCommit = null;
private Savepoint savePoint = null;
 
@Override
public boolean canRestoreState() {
107,6 → 137,8
this.autoCommit = conn.getAutoCommit();
if (this.autoCommit) {
conn.setAutoCommit(false);
} else if (!continueTx) {
this.savePoint = conn.setSavepoint();
}
}
 
118,13 → 150,32
@Override
public void restoreState(Connection conn) throws SQLException {
// can be null if getAutoCommit() failed, in that case nothing to do
if (this.autoCommit == Boolean.TRUE) {
final boolean hasStoppedAutoCommit = Boolean.TRUE.equals(this.autoCommit);
final boolean hasSavePoint = this.savePoint != null;
// at most one is enough (otherwise change if/else below)
assert !(hasStoppedAutoCommit && hasSavePoint) : "Begun a transaction and created a save point";
if (hasStoppedAutoCommit || hasSavePoint) {
// true if the exception was thrown by get()
boolean getExn = true;
try {
this.get();
getExn = false;
if (hasStoppedAutoCommit)
conn.commit();
// MS SQL cannot release save points
// http://technet.microsoft.com/en-us/library/ms378791.aspx
else if (ds.getSystem() != SQLSystem.MSSQL)
conn.releaseSavepoint(this.savePoint);
} catch (Exception e) {
if (hasStoppedAutoCommit)
conn.rollback();
else
conn.rollback(this.savePoint);
// if the exception wasn't generated by get() the caller must be notified
if (!getExn)
throw new SQLException("Couldn't " + (hasSavePoint ? "release save point" : "commit"), e);
} finally {
if (hasStoppedAutoCommit)
conn.setAutoCommit(true);
}
}
280,7 → 331,7
* @return the results of the handlers.
* @throws SQLException if an error occur
* @throws RTInterruptedException if the current thread is interrupted.
* @see {@link SQLSystem#isMultipleResultSetsSupported()}
* @see SQLSystem#isMultipleResultSetsSupported()
*/
static public List<?> executeMultiple(final DBSystemRoot sysRoot, final List<String> queries, final List<? extends ResultSetHandler> handlers) throws SQLException, RTInterruptedException {
final int size = handlers.size();
/trunk/OpenConcerto/src/org/openconcerto/sql/utils/ChangeBase.java
67,7 → 67,7
final Class<? extends Changer> c = this.getChange() == null ? null : this.getChange().findClass(converter);
if (c != null) {
try {
this.getChange().exec(c, params);
this.getChange().exec(getRoot(), c, params);
} catch (SQLException e) {
e.printStackTrace();
}
/trunk/OpenConcerto/src/org/openconcerto/sql/utils/BackupPanel.java
18,6 → 18,7
import org.openconcerto.sql.model.DBRoot;
import org.openconcerto.sql.model.DBSystemRoot;
import org.openconcerto.sql.model.SQLSystem;
import org.openconcerto.sql.users.rights.UserRightsManager;
import org.openconcerto.ui.DefaultGridBagConstraints;
import org.openconcerto.ui.JLabelBold;
import org.openconcerto.ui.ReloadPanel;
39,6 → 40,7
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.List;
 
56,6 → 58,8
 
public class BackupPanel extends JPanel implements ActionListener {
 
public static final String RIGHT_CODE = "BACKUP";
 
private final DateFormat format = new SimpleDateFormat("EEEE", getTM().getTranslationsLocale());
 
private JProgressBar barDB = new JProgressBar();
75,6 → 79,7
 
BackupProps props;
 
// listDB null means all roots of conf
public BackupPanel(List<String> listDB, List<File> dirs2Save, boolean startNow, BackupProps props) {
super(new GridBagLayout());
GridBagConstraints c = new DefaultGridBagConstraints();
87,6 → 92,8
// TODO SHOW ERRORS
 
this.listDb = listDB;
if (dirs2Save == null)
throw new NullPointerException("Null dirs");
this.dirs2save = dirs2Save;
 
JPanel topPanel = new JPanel(new VFlowLayout(VFlowLayout.MIDDLE, 4, 2, true));
190,7 → 197,10
 
JPanel panelButton = new JPanel();
this.buttonClose = new JButton(getTM().translate("close"));
if (!startNow) {
final boolean canBackup = UserRightsManager.getCurrentUserRights().haveRight(RIGHT_CODE);
this.buttonBackup.setEnabled(canBackup);
// display the disabled button since the backup won't happen
if (!startNow || !canBackup) {
panelButton.add(this.buttonBackup);
}
panelButton.add(this.buttonClose);
250,8 → 260,10
}
 
public final void sauvegarde() {
// also check right (set in the constructor)
if (!this.buttonBackup.isEnabled())
return;
assert UserRightsManager.getCurrentUserRights().haveRight(RIGHT_CODE);
 
this.barDB.setStringPainted(false);
this.buttonBackup.setEnabled(false);
302,7 → 314,7
props.setProperty("LastBackupDestination", BackupPanel.this.textDest.getText());
 
// Sauvegarde de la base
if (BackupPanel.this.listDb != null) {
if (BackupPanel.this.listDb == null || BackupPanel.this.listDb.size() > 0) {
 
final DBRoot root = Configuration.getInstance().getRoot();
final DBSystemRoot sysRoot = root.getDBSystemRoot();
310,9 → 322,9
 
// Sauvegarde pour H2
if (system == SQLSystem.H2) {
// always backup everything for H2 (for others better to backup on
// the server)
sysRoot.getDataSource().execute("backup to " + root.getBase().quoteString(new File(fDest, "Base.zip").getAbsolutePath()));
// TODO don't backup H2 files below ('backup to' is the only safe
// way);
} else {
// Sauvegarde autres bases
File fBase = new File(fDest, "Base");
319,10 → 331,10
Copy copy;
try {
copy = new Copy(true, fBase, sysRoot, false, false);
for (String db : BackupPanel.this.listDb) {
final Collection<String> rootsToBackup = BackupPanel.this.listDb == null ? sysRoot.getChildrenNames() : BackupPanel.this.listDb;
for (String db : rootsToBackup) {
copy.applyTo(db, null);
}
 
} catch (SQLException e) {
e.printStackTrace();
errors++;
/trunk/OpenConcerto/src/org/openconcerto/sql/utils/SQLCreateTable.java
15,7 → 15,6
 
import org.openconcerto.sql.model.DBRoot;
import org.openconcerto.sql.model.SQLBase;
import org.openconcerto.sql.model.SQLName;
import org.openconcerto.sql.model.SQLSyntax;
import org.openconcerto.sql.model.SQLSystem;
import org.openconcerto.sql.model.SQLTable;
86,7 → 85,7
if (!this.plain) {
genClauses.add(0, SQLBase.quoteIdentifier(SQLSyntax.ID_NAME) + this.getSyntax().getPrimaryIDDefinition());
genClauses.add(SQLBase.quoteIdentifier(SQLSyntax.ARCHIVE_NAME) + this.getSyntax().getArchiveDefinition());
// MS treat all NULL equals contrary to the standard
// MS unique constraint is not standard so add it in modifyOutClauses()
if (getSyntax().getSystem() == SQLSystem.MSSQL) {
genClauses.add(SQLBase.quoteIdentifier(SQLSyntax.ORDER_NAME) + this.getSyntax().getOrderType() + " DEFAULT " + this.getSyntax().getOrderDefault());
} else {
99,18 → 98,7
protected void modifyOutClauses(List<DeferredClause> clauses) {
super.modifyOutClauses(clauses);
if (!this.plain && getSyntax().getSystem() == SQLSystem.MSSQL) {
clauses.add(new OutsideClause() {
@Override
public ClauseType getType() {
return ClauseType.ADD_INDEX;
clauses.add(this.createUniquePartialIndex("orderIdx", Collections.singletonList(SQLSyntax.ORDER_NAME), null));
}
 
@Override
public String asString(SQLName tableName) {
return "create unique index idx on " + tableName.quote() + "(" + SQLBase.quoteIdentifier(SQLSyntax.ORDER_NAME) + ") where " + SQLBase.quoteIdentifier(SQLSyntax.ORDER_NAME)
+ " is not null";
}
});
}
}
}
/trunk/OpenConcerto/src/org/openconcerto/sql/utils/CorrectBase.java
134,7 → 134,7
 
@Override
protected Change getChange() {
return new Correct(this.getRoot());
return new Correct();
}
 
static public void main(String[] args) throws IOException {
/trunk/OpenConcerto/src/org/openconcerto/sql/utils/ChangeTable.java
14,6 → 14,7
package org.openconcerto.sql.utils;
 
import static java.util.Collections.singletonList;
import org.openconcerto.sql.Log;
import org.openconcerto.sql.model.SQLBase;
import org.openconcerto.sql.model.SQLField;
import org.openconcerto.sql.model.SQLName;
21,17 → 22,21
import org.openconcerto.sql.model.SQLSystem;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.SQLTable.Index;
import org.openconcerto.sql.model.SQLTable.SQLIndex;
import org.openconcerto.sql.model.SQLType;
import org.openconcerto.sql.model.Where;
import org.openconcerto.sql.model.graph.Link;
import org.openconcerto.sql.model.graph.Link.Rule;
import org.openconcerto.sql.model.graph.SQLKey;
import org.openconcerto.utils.CollectionMap;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.ListMap;
import org.openconcerto.utils.ReflectUtils;
import org.openconcerto.utils.StringUtils;
import org.openconcerto.utils.cc.ITransformer;
 
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
40,6 → 45,7
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;
 
/**
* Construct a statement about a table.
51,6 → 57,39
*/
public abstract class ChangeTable<T extends ChangeTable<T>> {
 
private static final String TRIGGER_SUFFIX = "_trigger";
protected static final String[] TRIGGER_EVENTS = { "INSERT", "UPDATE" };
 
// group 1 is the columns, group 2 the where
public static final Pattern H2_UNIQUE_TRIGGER_PATTERN = Pattern.compile("\\snew " + PartialUniqueTrigger.class.getName() + "\\(\\s*java.util.Arrays.asList\\((.+)\\)\\s*,(.+)\\)");
public static final Pattern H2_LIST_PATTERN = Pattern.compile("\\s*,\\s*");
 
public static final String MYSQL_TRIGGER_SUFFIX_1 = getTriggerSuffix(TRIGGER_EVENTS[0]);
public static final String MYSQL_TRIGGER_SUFFIX_2 = getTriggerSuffix(TRIGGER_EVENTS[1]);
public static final String MYSQL_FAKE_PROCEDURE = "Unique constraint violation";
public static final String MYSQL_TRIGGER_EXCEPTION = "call " + SQLBase.quoteIdentifier(MYSQL_FAKE_PROCEDURE);
// group 1 is the table name, group 2 the where
public static final Pattern MYSQL_UNIQUE_TRIGGER_PATTERN = Pattern.compile("IF\\s*\\(\\s*" + Pattern.quote("SELECT COUNT(*)") + "\\s+FROM\\s+(.+)\\s+where\\s+(.+)\\)\\s*>\\s*1\\s+then\\s+"
+ Pattern.quote(MYSQL_TRIGGER_EXCEPTION), Pattern.CASE_INSENSITIVE);
// to split the where
public static final Pattern MYSQL_WHERE_PATTERN = Pattern.compile("\\s+and\\s+", Pattern.CASE_INSENSITIVE);
// to find the column name
public static final Pattern MYSQL_WHERE_EQ_PATTERN = Pattern.compile("(NEW.)?(.+)\\s*=\\s*(NEW.)?\\2");
 
public static final String getIndexName(final String triggerName, final SQLSystem system) {
if (system == SQLSystem.MYSQL && triggerName.endsWith(MYSQL_TRIGGER_SUFFIX_1)) {
return triggerName.substring(0, triggerName.length() - MYSQL_TRIGGER_SUFFIX_1.length());
} else if (system == SQLSystem.H2 && triggerName.endsWith(TRIGGER_SUFFIX)) {
return triggerName.substring(0, triggerName.length() - TRIGGER_SUFFIX.length());
} else {
return null;
}
}
 
static private String getTriggerSuffix(final String event) {
return (event == null ? "" : '_' + event.toLowerCase()) + TRIGGER_SUFFIX;
}
 
public static enum ClauseType {
ADD_COL, ADD_CONSTRAINT, ADD_INDEX, DROP_COL, DROP_CONSTRAINT, DROP_INDEX, ALTER_COL, OTHER
}
357,9 → 396,9
private String rootName, name;
private final SQLSyntax syntax;
private final List<FCSpec> fks;
private final CollectionMap<ClauseType, String> clauses;
private final CollectionMap<ClauseType, DeferredClause> inClauses;
private final CollectionMap<ClauseType, DeferredClause> outClauses;
private final ListMap<ClauseType, String> clauses;
private final ListMap<ClauseType, DeferredClause> inClauses;
private final ListMap<ClauseType, DeferredClause> outClauses;
 
public ChangeTable(final SQLSyntax syntax, final String rootName, final String name) {
super();
367,9 → 406,9
this.rootName = rootName;
this.name = name;
this.fks = new ArrayList<FCSpec>();
this.clauses = new CollectionMap<ClauseType, String>();
this.inClauses = new CollectionMap<ClauseType, DeferredClause>();
this.outClauses = new CollectionMap<ClauseType, DeferredClause>();
this.clauses = new ListMap<ClauseType, String>();
this.inClauses = new ListMap<ClauseType, DeferredClause>();
this.outClauses = new ListMap<ClauseType, DeferredClause>();
 
// check that (T) this; will succeed
if (this.getClass() != ReflectUtils.getTypeArguments(this, ChangeTable.class).get(0))
405,14 → 444,14
if (this.getSyntax() != ct.getSyntax())
throw new IllegalArgumentException("not same syntax: " + this.getSyntax() + " != " + ct.getSyntax());
this.setName(ct.getName());
for (final Entry<ClauseType, Collection<String>> e : ct.clauses.entrySet()) {
for (final Entry<ClauseType, ? extends Collection<String>> e : ct.clauses.entrySet()) {
for (final String s : e.getValue())
this.addClause(s, e.getKey());
}
for (final DeferredClause c : ct.inClauses.values()) {
for (final DeferredClause c : ct.inClauses.allValues()) {
this.addClause(c);
}
for (final DeferredClause c : ct.outClauses.values()) {
for (final DeferredClause c : ct.outClauses.allValues()) {
this.addOutsideClause(c);
}
for (final FCSpec fk : ct.fks) {
428,8 → 467,33
* @param name the name of the column.
* @param count the number of char.
* @return this.
* @throws IllegalArgumentException if <code>count</code> is too high.
*/
public final T addVarCharColumn(String name, int count) {
return this.addVarCharColumn(name, count, false);
}
 
/**
* Adds a varchar column not null and with '' as the default.
*
* @param name the name of the column.
* @param count the number of characters.
* @param lenient <code>true</code> if <code>count</code> should be restricted to the maximum
* allowed value of the system, <code>false</code> will throw an exception.
* @return this.
* @throws IllegalArgumentException if <code>count</code> is too high and <code>lenient</code>
* is <code>false</code>.
*/
public final T addVarCharColumn(final String name, int count, final boolean lenient) throws IllegalArgumentException {
final int max = getSyntax().getMaximumVarCharLength();
if (count > max) {
if (lenient) {
Log.get().fine("Truncated " + name + " from " + count + " to " + max);
count = max;
} else {
throw new IllegalArgumentException("Count too high : " + count + " > " + max);
}
}
return this.addColumn(name, "varchar(" + count + ") default '' NOT NULL");
}
 
480,7 → 544,7
* @see SQLSyntax#getTypeNames(Class)
*/
public final <N extends Number> T addNumberColumn(String name, Class<N> javaType, N defaultVal, boolean nullable) {
final Set<String> typeNames = getSyntax().getTypeNames(javaType);
final Collection<String> typeNames = getSyntax().getTypeNames(javaType);
if (typeNames.size() == 0)
throw new IllegalArgumentException(javaType + " isn't supported by " + getSyntax());
return this.addColumn(name, typeNames.iterator().next(), getNumberDefault(defaultVal), nullable);
661,12 → 725,36
}
 
public T addUniqueConstraint(final String name, final List<String> cols) {
// for many systems (at least pg & h2) constraint names must be unique in a schema
return this.addUniqueConstraint(name, cols, null);
}
 
/**
* Add a unique constraint. If the table already exists, an initial check will be performed. As
* per the standard <code>NULL</code> means unknown and therefore equal with nothing.
* <p>
* NOTE: on some systems, an index or even triggers will be created instead (particularly with a
* where).
* </p>
*
* @param name name of the constraint.
* @param cols the columns of the constraint, e.g. ["DESIGNATION"].
* @param where an optional where to limit the rows checked, can be <code>null</code>, e.g.
* "not ARCHIVED".
* @return this.
*/
public T addUniqueConstraint(final String name, final List<String> cols, final String where) {
final int size = cols.size();
if (size == 0)
throw new IllegalArgumentException("No cols");
final SQLSystem system = getSyntax().getSystem();
// MS treat all NULL equals contrary to the standard
if (system == SQLSystem.MSSQL) {
return this.addOutsideClause(createUniquePartialIndex(name, cols, where));
} else if (where == null) {
return this.addClause(new DeferredClause() {
@Override
public String asString(ChangeTable<?> ct, SQLName tableName) {
final String constrName = SQLSyntax.getSchemaUniqueName(tableName.getName(), name);
return ct.getConstraintPrefix() + "CONSTRAINT " + SQLBase.quoteIdentifier(constrName) + " UNIQUE (" + SQLSyntax.quoteIdentifiers(cols) + ")";
return ct.getConstraintPrefix() + "CONSTRAINT " + getQuotedConstraintName(tableName, name) + " UNIQUE (" + SQLSyntax.quoteIdentifiers(cols) + ")";
}
 
@Override
674,8 → 762,189
return ClauseType.ADD_CONSTRAINT;
}
});
} else if (system == SQLSystem.POSTGRESQL) {
return this.addOutsideClause(createUniquePartialIndex(name, cols, where));
} else if (system == SQLSystem.H2) {
// initial select to check uniqueness
if (this instanceof AlterTable) {
// TODO should implement SIGNAL instead of abusing CSVREAD
this.addOutsideClause(new DeferredClause() {
@Override
public ClauseType getType() {
return ClauseType.OTHER;
}
 
@Override
public String asString(ChangeTable<?> ct, SQLName tableName) {
final String select = getInitialCheckSelect(cols, where, tableName);
return "SELECT CASE WHEN (" + select + ") > 0 then CSVREAD('Unique constraint violation') else 'OK' end case;";
}
});
}
final String javaWhere = StringUtils.doubleQuote(where);
final String javaCols = "java.util.Arrays.asList(" + CollectionUtils.join(cols, ", ", new ITransformer<String, String>() {
@Override
public String transformChecked(final String col) {
return StringUtils.doubleQuote(col);
}
}) + ")";
final String body = "AS $$ org.h2.api.Trigger create(){ return new " + PartialUniqueTrigger.class.getName() + "(" + javaCols + ", " + javaWhere + "); } $$";
assert H2_UNIQUE_TRIGGER_PATTERN.matcher(body).find();
return this.addOutsideClause(new UniqueTrigger(name, Arrays.asList(TRIGGER_EVENTS)) {
@Override
protected String getBody(SQLName tableName) {
return body;
}
});
} else if (system == SQLSystem.MYSQL) {
// initial select to check uniqueness
if (this instanceof AlterTable) {
this.addOutsideClause(new DeferredClause() {
@Override
public ClauseType getType() {
return ClauseType.OTHER;
}
 
@Override
public String asString(ChangeTable<?> ct, SQLName tableName) {
final String procName = SQLBase.quoteIdentifier("checkUniqueness_" + tableName.getName());
String res = "DROP PROCEDURE IF EXISTS " + procName + ";\n";
res += "CREATE PROCEDURE " + procName + "() BEGIN\n";
final String select = getInitialCheckSelect(cols, where, tableName);
// don't put newline right after semicolon to avoid splitting here
res += "IF (" + select + ") > 0 THEN " + MYSQL_TRIGGER_EXCEPTION + "; END IF; \n";
res += "END;\n";
res += "CALL " + procName + ";";
return res;
}
});
}
final UniqueTrigger trigger = new UniqueTrigger(name, Arrays.asList(TRIGGER_EVENTS[0])) {
@Override
protected String getBody(final SQLName tableName) {
final String body = "BEGIN IF " + getNotNullWhere(cols, "NEW.") + " THEN\n" +
//
"IF ( SELECT COUNT(*) from " + tableName + " where " + where + " and " + CollectionUtils.join(cols, " and ", new ITransformer<String, String>() {
@Override
public String transformChecked(String col) {
return SQLBase.quoteIdentifier(col) + " = NEW." + SQLBase.quoteIdentifier(col);
}
}) + ") > 1 then\n" + MYSQL_TRIGGER_EXCEPTION + "; END IF; \n"
// don't put newline right after semicolon to avoid splitting here
+ "END IF; \n" + "END";
return body;
}
};
this.addOutsideClause(trigger);
for (int i = 1; i < TRIGGER_EVENTS.length; i++) {
this.addOutsideClause(new UniqueTrigger(name, Arrays.asList(TRIGGER_EVENTS[i])) {