package org.simantics.district.network.techtype; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import org.simantics.Simantics; import org.simantics.databoard.binding.NumberBinding; import org.simantics.databoard.binding.error.BindingException; import org.simantics.databoard.type.Datatype; import org.simantics.databoard.type.NumberType; import org.simantics.databoard.type.StringType; import org.simantics.databoard.util.Range; import org.simantics.databoard.util.RangeException; import org.simantics.db.ReadGraph; import org.simantics.db.Resource; import org.simantics.db.Statement; import org.simantics.db.common.procedure.adapter.TransientCacheListener; import org.simantics.db.common.request.IndexRoot; import org.simantics.db.common.request.ResourceRead; import org.simantics.db.common.request.UniqueRead; import org.simantics.db.exception.DatabaseException; import org.simantics.db.layer0.QueryIndexUtils; import org.simantics.db.layer0.request.PropertyInfo; import org.simantics.db.layer0.request.PropertyInfoRequest; import org.simantics.district.network.ontology.DistrictNetworkResource; import org.simantics.district.network.techtype.requests.PossibleTechTypeKeyName; import org.simantics.district.network.techtype.requests.TechTypeTableData; import org.simantics.layer0.Layer0; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TechTypeValidationUtils { private static final Logger LOGGER = LoggerFactory.getLogger(TechTypeValidationUtils.class); /** * Return a set of invalid tech type table entries. * * The tech type table values are validated against any limits that have been * defined for associated component type properties. * * Invalid entries are designated by strings of the form "/". * * @param table A tech type table resource * @return A set of labels for invalid values * @throws DatabaseException */ public static Set validateTechTypeTable(Resource table) throws DatabaseException { LOGGER.trace("Validating resource table {}", table); // Use a unique read - we don't want to pollute the cache with this return Simantics.getSession().syncRequest(new UniqueRead>() { @Override public Set perform(ReadGraph graph) throws DatabaseException { Resource type = graph.getPossibleObject(table, DistrictNetworkResource.getInstance(graph).TechType_TechTypeTable_HasComponentType); if (type == null) return Collections.emptySet(); Map props = graph.syncRequest(new PropertyInfoMapOfType(type), TransientCacheListener.instance()); Set result = new HashSet<>(); Map> data = graph.syncRequest(new TechTypeTableData(table), TransientCacheListener.instance()); for (String code : data.keySet()) { LOGGER.trace(" type code {}", code); Map values = data.get(code); for (String propertyName : values.keySet()) { PropertyInfo info = props.get(propertyName); // Allow property names to start with an underscore if (info == null) info = props.get("_" + propertyName); if (info == null) { LOGGER.trace(" {} - no property", propertyName); continue; } Datatype dt = info.requiredDatatype; if (dt == null || !(dt instanceof NumberType)) continue; // Only check ranges of numerical properties String range = dt.metadata.get("range"); if (range != null) { Range rng; try { rng = Range.valueOf(range); } catch (RangeException e1) { LOGGER.error("Invalid range string {} for property {}", range, propertyName, e1); continue; } Number value = getPossibleNumericValue(info, values.get(propertyName)); if (value == null) { // Nothing to do here, treat non-numeric strings as missing values LOGGER.trace(" {} - no value {} / {}", propertyName, values.get(propertyName), range); } else if (!rng.contains(value)) { LOGGER.trace(" {} - range violation {} / {}", propertyName, values.get(propertyName), range); result.add(code + "/" + propertyName); } else { LOGGER.trace(" {} - valid value {} / {}", propertyName, values.get(propertyName), range); } } } } return result; } }); } /** * Find all component properties that do not match the value given in a tech type table. * * @param table A TechTypeTable instance * @return Lists of all non-matching properties, indexed by component resource * @throws DatabaseException */ public static Map> findConsistencyViolations(Resource table) throws DatabaseException { LOGGER.trace("Validating resource table {}", table); // Use a unique read - we don't want to pollute the cache with this return Simantics.getSession().syncRequest(new UniqueRead>>() { @Override public Map> perform(ReadGraph graph) throws DatabaseException { Resource type = graph.getPossibleObject(table, DistrictNetworkResource.getInstance(graph).TechType_TechTypeTable_HasComponentType); if (type == null) return Collections.emptyMap(); Resource model = graph.syncRequest(new IndexRoot(table)); Map props = graph.syncRequest(new PropertyInfoMapOfType(type), TransientCacheListener.instance()); String keyName = graph.syncRequest(new PossibleTechTypeKeyName(type)); PropertyInfo keyPredicate = props.get(keyName); if (keyName.startsWith("_")) keyName = keyName.substring(1); Map> data = graph.syncRequest(new TechTypeTableData(table), TransientCacheListener.instance()); Map> result = new HashMap<>(); for (Resource component : QueryIndexUtils.searchByType(graph, model, type)) { String key = graph.getRelatedValue2(component, keyPredicate.predicate); Map values = data.get(key); if (values == null) { // Highlight the missing tech type key addMapListItem(result, component, keyPredicate); continue; } for (PropertyInfo prop : props.values()) { if (!prop.isHasProperty) continue; String valueString = values.get(prop.name); if (valueString == null) continue; Object value = null; if (prop.requiredDatatype instanceof NumberType) { value = getPossibleNumericValue(prop, valueString); } else if (prop.requiredDatatype instanceof StringType) { value = valueString; } else { continue; } if (value == null) { Statement statement = graph.getPossibleStatement(component, prop.predicate); if (statement != null && statement.getObject().equals(component)) { addMapListItem(result, component, prop); } continue; } Object currentValue = graph.getRelatedValue2(component, prop.predicate); if (!Objects.equals(value, currentValue)) { addMapListItem(result, component, prop); } } } return result; } }); } private static Number getPossibleNumericValue(PropertyInfo info, String valueString) { try { Double num = Double.valueOf(valueString.replace(",", ".")); NumberBinding binding = (NumberBinding)info.defaultBinding; Number value = (Number) binding.create(num); return value; } catch (NumberFormatException e) { return null; } catch (BindingException e) { LOGGER.error("Binding error for property {}", info.name, e); return null; } } private static void addMapListItem(Map> result, A a, B b) { List list = result.get(a); if (list == null) { list = new ArrayList<>(); result.put(a, list); } list.add(b); } private static class PropertyInfoMapOfType extends ResourceRead> { protected PropertyInfoMapOfType(Resource type) { super(type); } @Override public Map perform(ReadGraph graph) throws DatabaseException { Map result = new HashMap(); for (Resource prop : graph.getObjects(resource, Layer0.getInstance(graph).DomainOf)) { PropertyInfo info = graph.syncRequest(new PropertyInfoRequest(prop), TransientCacheListener.instance()); result.put(info.name, info); } return result; } } }