/* * Constellation - An open source and standard compliant SDI * http://www.constellation-sdi.org * * Copyright 2014 Geomatys. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.constellation.metadata.io; // J2SE dependencies import org.apache.sis.internal.jaxb.LegacyNamespaces; import org.apache.sis.metadata.iso.extent.DefaultGeographicDescription; import org.apache.sis.referencing.NamedIdentifier; import org.apache.sis.util.iso.DefaultInternationalString; import org.apache.sis.xml.IdentifiedObject; import org.apache.sis.xml.IdentifierSpace; import org.apache.sis.xml.XLink; import org.apache.sis.xml.XLink.Type; import org.apache.sis.xml.XML; import org.constellation.generic.database.Automatic; import org.constellation.generic.database.BDD; import org.constellation.jaxb.MarshallWarnings; import org.constellation.metadata.utils.Utils; import org.constellation.util.ReflectionUtilities; import org.geotoolkit.ebrim.xml.EBRIMMarshallerPool; import org.geotoolkit.util.StringUtilities; import org.mdweb.io.MD_IOException; import org.mdweb.io.MD_IOFactory; import org.mdweb.io.Writer; import org.mdweb.model.profiles.Profile; import org.mdweb.model.schemas.Classe; import org.mdweb.model.schemas.CodeList; import org.mdweb.model.schemas.CodeListElement; import org.mdweb.model.schemas.Path; import org.mdweb.model.schemas.PrimitiveType; import org.mdweb.model.schemas.Property; import org.mdweb.model.schemas.Standard; import org.mdweb.model.storage.FullRecord; import org.mdweb.model.storage.LinkedValue; import org.mdweb.model.storage.RecordInfo; import org.mdweb.model.storage.RecordSet; import org.mdweb.model.storage.RecordSet.EXPOSURE; import org.mdweb.model.storage.TextValue; import org.mdweb.model.storage.Value; import org.mdweb.model.users.User; import org.opengis.annotation.UML; import org.opengis.metadata.Metadata; import org.w3c.dom.Node; import javax.imageio.spi.ServiceRegistry; import javax.sql.DataSource; import javax.xml.bind.JAXBElement; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import java.io.StringReader; import java.io.StringWriter; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.URI; import java.sql.Date; import java.sql.SQLException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.MissingResourceException; import java.util.TimeZone; import java.util.logging.Level; // constellation dependencies // Geotoolkit dependencies // Apache dependencies // MDWeb dependencies // GeoAPI /** * * @author Guilhem Legal (Geomatys) */ public class MDWebMetadataWriter extends AbstractMetadataWriter { /** * A MDWeb RecordSets where write the record. */ private RecordSet mdRecordSet; /** * The MDWeb user who owe the inserted record. */ private final User defaultUser; /** * A writer to the MDWeb database. */ protected Writer mdWriter; /** * The current main standard of the Object to create */ private Standard mainStandard; /** * A map recording the binding between java Class and MDWeb {@link classe} */ private final Map<String, Classe> classBinding = new HashMap<>(); /** * A List of contact record. */ private final Map<Object, Value> contacts = new HashMap<>(); /** * A flag indicating that we don't want to write predefined values. */ private boolean noLink = false; /** * A flag indicating that we don't want to add the metadata to the index. */ protected final boolean noIndexation; private final Map<Standard, List<Standard>> standardMapping = new HashMap<>(); private static final TimeZone tz = TimeZone.getTimeZone("GMT+2:00"); /** * Record the date format in the metadata. */ protected static final List<DateFormat> DATE_FORMAT = new ArrayList<>(); static { DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); df.setTimeZone(tz); DATE_FORMAT.add(df); df = new SimpleDateFormat("yyyy-MM-dd"); df.setTimeZone(tz); DATE_FORMAT.add(df); } /** * Build a new metadata writer. * * @param configuration The configuration object. * @throws org.constellation.metadata.io.MetadataIoException */ public MDWebMetadataWriter(final Automatic configuration) throws MetadataIoException { super(); if (configuration == null) { throw new MetadataIoException("The configuration object is null"); } // we get the database informations final BDD db = configuration.getBdd(); if (db == null) { throw new MetadataIoException("The configuration file does not contains a BDD object"); } try { final DataSource dataSource = db.getDataSource(); final boolean isPostgres = db.getClassName().equals("org.postgresql.Driver"); MD_IOFactory factory = null; final Iterator<MD_IOFactory> ite = ServiceRegistry.lookupProviders(MD_IOFactory.class); while (ite.hasNext()) { MD_IOFactory currentFactory = ite.next(); if (currentFactory.matchImplementationType(dataSource, isPostgres)) { factory = currentFactory; } } if (factory != null) { mdWriter = factory.getPooledInstance(db.getConnectURL(), dataSource, isPostgres); mdRecordSet = getRecordSet(configuration.getDefaultRecordSet()); defaultUser = mdWriter.getUser("admin"); if ("true".equalsIgnoreCase(configuration.getNoIndexation())) { noIndexation = true; LOGGER.info("indexation is de-activated for Transactionnal part"); } else { noIndexation = false; } initStandardMapping(); initContactMap(); } else { throw new MetadataIoException("unable to find a MD_IO factory"); } } catch (MD_IOException ex) { throw new MetadataIoException("MD_IOException while initializing the MDWeb writer:" +'\n'+ "cause:" + ex.getMessage()); } catch (SQLException ex) { throw new MetadataIoException("SQLException while initializing the MDWeb writer:" +'\n'+ "cause:" + ex.getMessage()); } } /** * Build a new metadata writer. * * @param mdWriter * @param defaultrecordSet * @param userLogin * * @throws org.constellation.metadata.io.MetadataIoException */ public MDWebMetadataWriter(final Writer mdWriter, final String defaultrecordSet, final String userLogin) throws MetadataIoException { super(); this.mdWriter = mdWriter; try { this.mdRecordSet = getRecordSet(defaultrecordSet); this.defaultUser = mdWriter.getUser(userLogin); initStandardMapping(); initContactMap(); } catch (MD_IOException ex) { throw new MetadataIoException("MD_IOException while getting the catalog and user:" +'\n'+ "cause:" + ex.getMessage()); } this.noIndexation = false; } public MDWebMetadataWriter(final Writer mdWriter) throws MetadataIoException { super(); this.mdWriter = mdWriter; try { this.mdRecordSet = RecordSet.DATA_RECORDSET; this.defaultUser = User.INTERNAL_USER; initStandardMapping(); initContactMap(); } catch (MD_IOException ex) { throw new MetadataIoException("MD_IOException while getting the catalog and user:" +'\n'+ "cause:" + ex.getMessage()); } this.noIndexation = false; } protected MDWebMetadataWriter() throws MetadataIoException { this.defaultUser = null; this.noIndexation = false; } private void initStandardMapping() { // ISO 19115 and its sub standard (ISO 19119, 19110) List<Standard> availableStandards = new ArrayList<>(); availableStandards.add(Standard.ISO_19115_FRA); availableStandards.add(Standard.ISO_19115); availableStandards.add(Standard.ISO_19115_2); availableStandards.add(Standard.ISO_19108); availableStandards.add(Standard.ISO_19103); availableStandards.add(Standard.ISO_19119); availableStandards.add(Standard.ISO_19110); availableStandards.add(Standard.MDWEB); standardMapping.put(Standard.ISO_19115, availableStandards); // CSW standard availableStandards = new ArrayList<>(); availableStandards.add(Standard.CSW); availableStandards.add(Standard.DUBLINCORE); availableStandards.add(Standard.DUBLINCORE_TERMS); availableStandards.add(Standard.OWS); standardMapping.put(Standard.CSW, availableStandards); // Ebrim v3 standard availableStandards = new ArrayList<>(); availableStandards.add(Standard.EBRIM_V3); availableStandards.add(Standard.CSW); availableStandards.add(Standard.OGC_FILTER); availableStandards.add(Standard.MDWEB); standardMapping.put(Standard.EBRIM_V3, availableStandards); // Ebrim v2.5 standard availableStandards = new ArrayList<>(); availableStandards.add(Standard.EBRIM_V2_5); availableStandards.add(Standard.CSW); availableStandards.add(Standard.OGC_FILTER); availableStandards.add(Standard.MDWEB); standardMapping.put(Standard.EBRIM_V2_5, availableStandards); // SensorML standard availableStandards.add(Standard.SENSORML); availableStandards.add(Standard.SENSOR_WEB_ENABLEMENT); availableStandards.add(Standard.ISO_19108); standardMapping.put(Standard.SENSORML, availableStandards); // we add the extra binding extracted from a properties file try { final Map<String, List<String>> extraStandard = new HashMap<>(); final Iterator<ExtraMappingFactory> ite = ServiceRegistry.lookupProviders(ExtraMappingFactory.class); while (ite.hasNext()) { final ExtraMappingFactory currentFactory = ite.next(); extraStandard.putAll(currentFactory.getExtraStandard()); } for (Entry<String, List<String>> entry : extraStandard.entrySet()) { final String mainStandardName = entry.getKey(); final Standard newMainStandard = mdWriter.getStandard(mainStandardName); if (newMainStandard == null) { LOGGER.log(Level.WARNING, "Unable to find the extra main standard:{0}", mainStandardName); continue; } final List<String> standardList = entry.getValue(); final List<Standard> standards = new ArrayList<>(); for (String standardName : standardList) { Standard standard = mdWriter.getStandard(standardName); if (standard == null) { LOGGER.log(Level.FINER, "Unable to find the extra standard:{0}", standardName); } else { standards.add(standard); } } if (standardMapping.containsKey(newMainStandard)) { final List<Standard> previousStandards = standardMapping.get(newMainStandard); previousStandards.addAll(standards); standardMapping.put(newMainStandard, previousStandards); } else { standardMapping.put(newMainStandard, standards); } } } catch (MD_IOException ex) { LOGGER.log(Level.WARNING, "MD_IO exception while reading extra standard properties for MDW meta writer", ex); } } // TODO move this to CSW implementation public RecordSet getRecordSet(final String defaultRecordSet) throws MD_IOException { RecordSet cat = null; if (defaultRecordSet != null) { cat = mdWriter.getRecordSet(defaultRecordSet); } if (cat == null) { cat = mdWriter.getRecordSet("CSWCat"); if (cat == null) { cat = new RecordSet("CSWCat", "CSW Data RecordSet", null, null, EXPOSURE.EXTERNAL, 0, new Date(System.currentTimeMillis()), true); mdWriter.writeRecordSet(cat); LOGGER.info("writing CSWCat"); } } return cat; } /** * Load the contact from MDweb database. * * @throws MD_IOException */ private void initContactMap() throws MD_IOException { final Collection<FullRecord> contactRecords = mdWriter.getContacts(); if (contactRecords.size() > 0) { LOGGER.log(Level.INFO, "initiazing {0} contacts", contactRecords.size()); final MDWebMetadataReader reader = new MDWebMetadataReader(mdWriter); for (FullRecord contactRecord : contactRecords) { Object responsibleParty = reader.getObjectFromRecord(null, contactRecord, MetadataType.ISO_19115); contacts.put(responsibleParty, contactRecord.getRoot()); } } } /** * Return an MDWeb {@link FullRecord} from an object. * * @param object The object to transform in record. * @return an MDWeb {@link FullRecord} representing the metadata object. * @throws org.mdweb.io.MD_IOException */ protected FullRecord getRecordFromObject(final Object object) throws MD_IOException { final String title = Utils.findTitle(object); return getRecordFromObject(object, defaultUser, mdRecordSet, null, title); } /** * Return an MDWeb {@link FullRecord} from an object. * * @param object The object to transform in record. * @param title * * @return an MDWeb {@link FullRecord} representing the metadata object. * @throws org.mdweb.io.MD_IOException */ protected FullRecord getRecordFromObject(final Object object, String title) throws MD_IOException { if (title == null) { title = Utils.findTitle(object); } return getRecordFromObject(object, defaultUser, mdRecordSet, null, title); } /** * Return an MDWeb {@link FullRecord} from an object. * * @param object The object to transform in record. * @param user * @param recordSet * @param title * @param profile * @return an MDWeb {@link FullRecord} representing the metadata object. * @throws org.mdweb.io.MD_IOException */ public FullRecord getRecordFromObject(final Object object, final User user, final RecordSet recordSet, Profile profile, String title) throws MD_IOException { if (user == null) { throw new MD_IOException("The User must not be null"); } if (object != null) { //we try to find a title for the from if ("unknow title".equals(title)) { title = mdWriter.getAvailableTitle(); } final Date creationDate = new Date(System.currentTimeMillis()); final String className = object.getClass().getSimpleName(); // ISO 19115 types if ("DefaultMetadata".equals(className) || // ISO 19115-2 types "MI_Metadata".equals(className) || // ISO 19110 types "FeatureCatalogueImpl".equals(className) || "FeatureOperationImpl".equals(className) || "FeatureAssociationImpl".equals(className) ) { mainStandard = Standard.ISO_19115; // CSW Types } else if ("RecordType".equals(className)) { mainStandard = Standard.CSW; // SML Types } else if ("SensorML".equals(className)) { mainStandard = Standard.SENSORML; // Ebrim Types } else if (object.getClass().getName().startsWith("org.geotoolkit.ebrim.xml.v300")) { mainStandard = Standard.EBRIM_V3; } else if (object.getClass().getName().startsWith("org.geotoolkit.ebrim.xml.v250")) { mainStandard = Standard.EBRIM_V2_5; // unkow types } else { mainStandard = Standard.ISO_19115; LOGGER.log(Level.WARNING, "Unknow object type:{0}, it may can''t be registered.", object.getClass().getName()); } final String identifier = Utils.findIdentifier(object); final FullRecord record = new FullRecord(-1, identifier, recordSet, title, user, null, profile, creationDate, creationDate, null, false, false, FullRecord.TYPE.NORMALRECORD); final Classe rootClasse = getClasseFromObject(object); if (rootClasse != null) { /** * A List of the already see object for the current metadata read * (in order to avoid infinite loop) */ final Map<Object, Value> alreadyWrite = new HashMap<>(); final Path rootPath = new Path(rootClasse.getStandard(), rootClasse); final List<Value> collection = addValueFromObject(record, object, rootPath, null, alreadyWrite); collection.clear(); alreadyWrite.clear(); return record; } else { LOGGER.log(Level.SEVERE, "unable to find the root class:{0}", object.getClass().getSimpleName()); return null; } } else { LOGGER.severe("unable to create record object is null"); return null; } } /** * Add a MDWeb value (and his children)to the specified record. * * @param record The created record. * @param object * @param path * @param parentValue * @param alreadyWrite * @return * @throws org.mdweb.io.MD_IOException * */ protected List<Value> addValueFromObject(final FullRecord record, Object object, Path path, final Value parentValue, final Map<Object, Value> alreadyWrite) throws MD_IOException { final List<Value> result = new ArrayList<>(); //if the path is not already in the database we write it if (mdWriter.getPath(path.getId()) == null) { mdWriter.writePath(path); } if (object == null) { return result; } //if the object is a JAXBElement we desencapsulate it if (object instanceof JAXBElement) { final JAXBElement jb = (JAXBElement) object; object = jb.getValue(); } //if the object is a collection we call the method on each child Classe classe; if (object instanceof Collection) { final Collection c = (Collection) object; for (Object obj: c) { if (path.getName().equals("geographicElement2") && obj instanceof DefaultGeographicDescription) { final String parentID = path.getParent().getId(); path = mdWriter.getPath(parentID + ":geographicElement3"); } result.addAll(addValueFromObject(record, obj, path, parentValue, alreadyWrite)); } return result; } else { classe = getClasseFromObject(object); } //if we don't have found the class we stop here if (classe == null) { return result; } //we try to find the good ordinal int ordinal; if (parentValue == null) { ordinal = 1; } else { ordinal = parentValue.getNewOrdinalForChild(path.getName()); } //we look if the object have been already write final Value linkedValue; if (contacts.get(object) != null) { linkedValue = contacts.get(object); } else if (isNoLink()) { linkedValue = null; } else { linkedValue = alreadyWrite.get(object); } //Special case for PT_FreeText if (classe.getName().equals("PT_FreeText")) { final DefaultInternationalString dis = (DefaultInternationalString) object; // 1. the root Value PT_FreeText final Value rootValue = new Value(path, record, ordinal, classe, parentValue, null); result.add(rootValue); // 2. The default value final String defaultValue = dis.toString(null); final Path defaultValuePath = new Path(path, classe.getPropertyByName("value")); final TextValue textValue = new TextValue(defaultValuePath, record , 1, defaultValue, mdWriter.getClasse("CharacterString", Standard.ISO_19103), rootValue, null); result.add(textValue); // 3. the localised values final Classe localisedString = mdWriter.getClasse("LocalisedCharacterString", Standard.ISO_19103); int localeOrdinal = 1; for (Locale locale : dis.getLocales()) { if (locale == null) {continue;} final Path valuePath = new Path(path, classe.getPropertyByName("textGroup")); final Value value = new Value(valuePath, record, localeOrdinal, localisedString, rootValue, null); result.add(value); final String localisedValue = dis.toString(locale); final Path locValuePath = new Path(valuePath, localisedString.getPropertyByName("value")); final TextValue locValValue = new TextValue(locValuePath, record , localeOrdinal, localisedValue, mdWriter.getClasse("CharacterString", Standard.ISO_19103), value, null); result.add(locValValue); final Path localePath = new Path(valuePath, localisedString.getPropertyByName("locale")); final String localeDesc = "#locale-" + locale.getISO3Language(); final TextValue localeValue = new TextValue(localePath, record , localeOrdinal, localeDesc, mdWriter.getClasse("CharacterString", Standard.ISO_19103), value, null); result.add(localeValue); localeOrdinal++; } //Special case for PT_Locale } else if (classe.getName().equals("PT_Locale")) { final Locale loc = (Locale) object; // 1. the root Value PT_Locale final Value rootValue = new Value(path, record, ordinal, classe, parentValue, null); result.add(rootValue); // 2. The languageCode value final String languageValue = loc.getLanguage(); final Path languageValuePath = new Path(path, classe.getPropertyByName("languageCode")); final TextValue lanTextValue = new TextValue(languageValuePath, record , ordinal, languageValue, mdWriter.getClasse("LanguageCode", Standard.ISO_19115), rootValue, null); result.add(lanTextValue); // 3. the country value final String countryValue = loc.getCountry(); final Path countryValuePath = new Path(path, classe.getPropertyByName("country")); final TextValue couTextValue = new TextValue(countryValuePath, record , ordinal, countryValue, mdWriter.getClasse("CountryCode", Standard.ISO_19115), rootValue, null); result.add(couTextValue); // 4. the encoding value "LOST for now" TODO // if its a primitive type we create a TextValue } else if (classe.isPrimitive() || classe.getName().equals("LocalName")) { if (classe instanceof CodeList) { final CodeList cl = (CodeList) classe; String codelistElement; if (classe.getName().equals("LanguageCode")) { try { codelistElement = ((Locale) object).getISO3Language(); } catch (MissingResourceException ex) { codelistElement = ((Locale) object).getLanguage(); } } else { if (object instanceof org.opengis.util.CodeList) { codelistElement = ((org.opengis.util.CodeList) object).identifier(); if (codelistElement == null) { codelistElement = ((org.opengis.util.CodeList) object).name(); } } else if (object.getClass().isEnum()) { codelistElement = ReflectionUtilities.getElementNameFromEnum(object); } else { LOGGER.log (Level.SEVERE, "{0} is not a codelist!", object.getClass().getName()); codelistElement = null; } } CodeListElement cle = (CodeListElement) cl.getPropertyByName(codelistElement); if (cle == null) { cle = (CodeListElement) cl.getPropertyByShortName(codelistElement); } if (cle instanceof org.mdweb.model.schemas.Locale) { object = cle.getShortName(); } else if (cle != null) { object = cle.getCode(); } else { final StringBuilder values = new StringBuilder(); for (Property p: classe.getProperties()) { values.append(p.getName()).append('\n'); } LOGGER.warning("unable to find a codeListElement named " + codelistElement + " in the codelist " + classe.getName() + "\nallowed values are:\n" + values); } } String value; if (object instanceof java.util.Date) { final java.util.Date d = (java.util.Date) object; Calendar c = new GregorianCalendar(); c.setTime(d); if (c.get(Calendar.HOUR) == 0 && c.get(Calendar.MINUTE) == 0 && c.get(Calendar.SECOND) == 0 && c.get(Calendar.MILLISECOND) == 0) { synchronized (DATE_FORMAT) { value = DATE_FORMAT.get(1).format(object) + "T00:00:00" + tz.getDisplayName().substring(3); } } else { synchronized (DATE_FORMAT) { value = DATE_FORMAT.get(0).format(object); } } } else if (object.getClass().isEnum()){ value = object.toString().toLowerCase(Locale.US); } else if (object instanceof URI){ value = object.toString(); value = value.replace("%5C", "\\"); } else { value = object.toString(); } final TextValue textValue = new TextValue(path, record , ordinal, value, classe, parentValue, null); result.add(textValue); // if we have already see this object we build a Linked Value. } else if (linkedValue != null) { final LinkedValue value = new LinkedValue(path, record, ordinal, linkedValue.getRecord(), linkedValue, classe, parentValue, null); result.add(value); // else we build a Value node. } else { final Value value = new Value(path, record, ordinal, classe, parentValue, null); result.add(value); //we add this object to the listed of already write element if (!isNoLink()) { alreadyWrite.put(object, value); } do { for (Property prop: classe.getProperties()) { // TODO remove when fix in MDweb2 if (prop.getName().equals("geographicElement3") || prop.getName().equals("geographicElement4")) { continue; } final String propName = specialCorrectionName(prop.getName(), object.getClass()); Method getter; if ("axis".equals(propName)) { getter = ReflectionUtilities.getMethod("get" + StringUtilities.firstToUpper(propName), object.getClass(), int.class); } else { getter = ReflectionUtilities.getGetterFromName(propName, object.getClass()); } if (getter != null) { try { final Object propertyValue; if ("axis".equals(propName)) { propertyValue = getter.invoke(object, 0); } else { propertyValue = getter.invoke(object); } if (propertyValue != null) { final Path childPath = new Path(path, prop); //if the path is not already in the database we write it if (mdWriter.getPath(childPath.getId()) == null) { mdWriter.writePath(childPath); } result.addAll(addValueFromObject(record, propertyValue, childPath, value, alreadyWrite)); } } catch (IllegalAccessException e) { LOGGER.severe("The class is not accessible"); return result; } catch (java.lang.reflect.InvocationTargetException e) { LOGGER.severe("Exception throw in the invokated getter: " + getter.toGenericString() + "\nCause: " + e.getMessage()); return result; } // special case for id } else if ("id".equals(propName) && object instanceof IdentifiedObject) { final Object propertyValue = ((IdentifiedObject)object).getIdentifierMap().getSpecialized(IdentifierSpace.ID); if (propertyValue != null) { final Path childPath = new Path(path, prop); //if the path is not already in the database we write it if (mdWriter.getPath(childPath.getId()) == null) { mdWriter.writePath(childPath); } result.addAll(addValueFromObject(record, propertyValue, childPath, value, alreadyWrite)); } } else if ("xLink".equals(propName) && object instanceof IdentifiedObject) { final Object propertyValue = ((IdentifiedObject)object).getIdentifierMap().getSpecialized(IdentifierSpace.XLINK); if (propertyValue != null) { final Path childPath = new Path(path, prop); //if the path is not already in the database we write it if (mdWriter.getPath(childPath.getId()) == null) { mdWriter.writePath(childPath); } result.addAll(addValueFromObject(record, propertyValue, childPath, value, alreadyWrite)); } } else if (!"unitOfMeasure".equals(propName) && !"verticalDatum".equals(propName)) { final Class valueClass = object.getClass(); final Object propertyValue = getValueFromField(valueClass, propName, object); if (propertyValue != null) { final Path childPath = new Path(path, prop); //if the path is not already in the database we write it if (mdWriter.getPath(childPath.getId()) == null) { mdWriter.writePath(childPath); } result.addAll(addValueFromObject(record, propertyValue, childPath, value, alreadyWrite)); } } else { LOGGER.warning("no getter found for:" + propName + " class: " + object.getClass().getName()); } } classe = classe.getSuperClass(); if (classe != null) { LOGGER.log(Level.FINER, "searching in superclasse {0}", classe.getName()); } } while (classe != null); } return result; } /** * Try to extract the value of a field named propName in the specified class (or any of its super class) * * @param valueClass A class. * @param propName The name of the searched field. * @param object the object on which we want to extract the field value. * * @return The value of the specified field or {@code null} */ private Object getValueFromField(Class valueClass, final String propName, final Object object) { final Class origClass = valueClass; do { try { final Field field = valueClass.getDeclaredField(propName); final Object propertyValue; if (field != null) { field.setAccessible(true); propertyValue = field.get(object); } else { propertyValue = null; } return propertyValue; } catch (NoSuchFieldException ex) { LOGGER.log(Level.FINER, "no such Field:" + propName + " in class:" + valueClass.getName()); } catch (SecurityException | IllegalAccessException ex) { LOGGER.log(Level.WARNING, null, ex); } valueClass = valueClass.getSuperclass(); } while (valueClass != null); LOGGER.log(Level.WARNING, "no such Field:" + propName + " in class:" + origClass.getName()); return null; } /** * apply special fix on the property name. * * @param attributeName * @param objectClass * @return */ public String specialCorrectionName(final String attributeName, final Class objectClass) { final String propName; // special case if (attributeName.equalsIgnoreCase("referenceSystemIdentifier") || (attributeName.equalsIgnoreCase("identifier") && objectClass.getSimpleName().equals("DefaultCoordinateSystemAxis")) || (attributeName.equalsIgnoreCase("identifier") && objectClass.getSimpleName().equals("DefaultVerticalCS")) || (attributeName.equalsIgnoreCase("identifier") && objectClass.getSimpleName().equals("DefaultVerticalDatum")) || (attributeName.equalsIgnoreCase("identifier") && objectClass.getSimpleName().equals("DefaultVerticalCRS"))) { propName = "name"; } else if (attributeName.equalsIgnoreCase("uom") && !objectClass.getSimpleName().equals("QuantityType") && !objectClass.getSimpleName().equals("QuantityRange") && !objectClass.getSimpleName().equals("TimeRange") && !objectClass.getSimpleName().equals("TimeType")) { propName = "unit"; } else if (attributeName.equalsIgnoreCase("verticalDatum") && objectClass.getSimpleName().equals("DefaultVerticalCRS")) { propName = "datum"; } else { propName = attributeName; } return propName; } /** * Return an MDWeb {@link Classe} object for the specified java object. * * @param object the object to identify * @return * * @throws org.mdweb.io.MD_IOException */ protected Classe getClasseFromObject(final Object object) throws MD_IOException { String className; String packageName; Classe result; if (object != null) { // special case variant (we don't want to use cache) for PT_Locale if (object instanceof Locale && ((Locale)object).getCountry() != null && !((Locale)object).getCountry().isEmpty()) { return mdWriter.getClasse("PT_Locale", Standard.ISO_19115); } // look for previously cached result result = classBinding.get(object.getClass().getName()); if (result != null) { return result; } // special case for the sub classe of Xlink if (object.getClass().equals(XLink.class)) { return mdWriter.getClasse("XLink", mdWriter.getStandard("Xlink")); } // special case for Xlink.Type enum if (object.getClass().equals(Type.class)) { return PrimitiveType.STRING; } // special case for NamedIdentifier if (object.getClass().equals(NamedIdentifier.class)) { return mdWriter.getClasse("RS_Identifier", Standard.ISO_19115); } //special case for Proxy: we extract the GeoAPI interface, then we get the UML annotation for className if (object.getClass().getSimpleName().startsWith("$Proxy")) { final Class apiInterface = object.getClass().getInterfaces()[0]; final UML a = (UML) apiInterface.getAnnotation(UML.class); className = a.identifier(); packageName = ""; } else { className = object.getClass().getSimpleName(); packageName = object.getClass().getPackage().getName(); } LOGGER.log(Level.FINER, "search for classe {0}", className); } else { return null; } //for the primitive type we return ISO primitive type result = getPrimitiveTypeFromName(className); if (result != null) { classBinding.put(object.getClass().getName(), result); return result; } final String annotationName = getNameFromAnnotation(object); if (annotationName != null) { className = annotationName; } else { //we remove the Default prefix if (className.startsWith("Default")) { className = className.substring(7, className.length()); } //we remove the Type suffix if (className.endsWith("Type") && !"CodeType".equals(className)){ className = className.substring(0, className.length() - 4); } } if (className.isEmpty()) { return null; } final List<Standard> availableStandards = standardMapping.get(mainStandard); if (availableStandards == null) { throw new IllegalArgumentException("Unexpected Main standard: " + mainStandard); } String availableStandardLabel = ""; for (Standard standard : availableStandards) { availableStandardLabel = availableStandardLabel + standard.getName() + ','; /* to avoid some confusion between to classes with the same name * we affect the standard in some special case */ if (packageName.startsWith("org.geotoolkit.sml.xml")) { standard = Standard.SENSORML; } else if (packageName.startsWith("org.geotoolkit.swe.xml")) { standard = Standard.SENSOR_WEB_ENABLEMENT; } else if ("org.geotoolkit.gml.xml.v311".equals(packageName)) { standard = Standard.ISO_19108; } String name = className; int nameType = 0; while (nameType < 3) { LOGGER.finer("searching: " + standard.getName() + ':' + name); result = mdWriter.getClasse(name, standard); if (result != null) { LOGGER.finer("class found:" + standard.getName() + ':' + name); classBinding.put(object.getClass().getName(), result); return result; } switch (nameType) { case 0: { name = "Time" + className; nameType = 1; break; } case 1: { name = "TM_" + className; nameType = 2; break; } default: nameType = 3; break; } } } availableStandardLabel = availableStandardLabel.substring(0, availableStandardLabel.length() - 1); LOGGER.warning("class not found: " + className + " in the following standards: " + availableStandardLabel + "\n (" + object.getClass().getName() + ')'); return null; } /** * Find The class name by extracting the {@link XmlRootElement} annotation. * For the instance of {@link org.opengis.util.CodeList}, * we extract the name from the {@link UML} annotation * * @param object A GeotoolKit object * @return the name parameter in the XmlElementRoot annotation or identifier parameter in UM annotation. * */ private String getNameFromAnnotation(final Object object) { if (object instanceof org.opengis.util.CodeList) { UML a = (UML)object.getClass().getAnnotation(UML.class); if (a != null) { return a.identifier(); } } else { XmlRootElement a = (XmlRootElement) object.getClass().getAnnotation(XmlRootElement.class); if (a != null) { return a.name(); } } return null; } /** * Return a {@link Classe} (java primitive type) from a class name. * * @param className the standard name of a class. * @return a primitive class. */ private Classe getPrimitiveTypeFromName(final String className) throws MD_IOException { final String mdwclassName; final Standard mdwStandard; if ("String".equals(className) || "SimpleInternationalString".equals(className) || "BaseUnit".equals(className)) { return PrimitiveType.STRING; } else if ("DefaultInternationalString".equalsIgnoreCase(className)) { mdwclassName = "PT_FreeText"; mdwStandard = Standard.ISO_19115; } else if ("Date".equalsIgnoreCase(className) || "URI".equalsIgnoreCase(className) || "Integer".equalsIgnoreCase(className) || "Boolean".equalsIgnoreCase(className)) { mdwclassName = className; mdwStandard = Standard.ISO_19103; } else if ("Long".equalsIgnoreCase(className)) { mdwclassName = "Integer"; mdwStandard = Standard.ISO_19103; } else if ("URL".equalsIgnoreCase(className)) { mdwclassName = className; mdwStandard = Standard.ISO_19115; //special case for locale codeList. } else if ("Locale".equals(className)) { mdwclassName = "LanguageCode"; mdwStandard = Standard.ISO_19115; //special case for Role codeList. } else if ("Double".equals(className)) { mdwclassName = "Real"; mdwStandard = Standard.ISO_19103; } else { return null; } final Classe candidate = PrimitiveType.getPrimitiveTypeFromName(mdwclassName, mdwStandard); final Classe result; if (candidate != null) { result = candidate; } else { result = mdWriter.getClasse(mdwclassName, mdwStandard); } if (result == null) { LOGGER.warning("The database does not contains the primitive type:" + mdwclassName + " in the standard:" + mdwStandard.getName()); } return result; } /** * {@inheritDoc} */ @Override public boolean storeMetadata(final Node obj) throws MetadataIoException { return storeMetadata(obj, null); } public boolean storeMetadata(final Node node, final String title) throws MetadataIoException { // profiling operation final long start = System.currentTimeMillis(); long transTime = 0; long writeTime = 0; // unmarshall the objet final Object obj = unmarshallObject(node); // we create a MDWeb record the object FullRecord record = null; Profile profile = null; try { // we try to determine the profile for the Object if ("org.geotoolkit.csw.xml.v202.RecordType".equals(obj.getClass().getName())) { profile = mdWriter.getProfile("DublinCore"); } else if (obj instanceof Metadata) { profile = mdWriter.getProfileByUrn(Utils.findStandardName(obj)); } final long startTrans = System.currentTimeMillis(); record = getRecordFromObject(obj, title); transTime = System.currentTimeMillis() - startTrans; if (record != null && mdWriter.isAlreadyUsedIdentifier(record.getIdentifier())) { throw new MD_IOException("The identifier " + record.getIdentifier() + " is already used"); } } catch (IllegalArgumentException e) { throw new MetadataIoException("This kind of resource cannot be parsed by the service: " + obj.getClass().getSimpleName() +'\n' + "cause: " + e.getMessage(), e, null); } catch (MD_IOException e) { throw new MetadataIoException("The service has throw an MD_IOException while transforming the metadata to a MDWeb object: " + e.getMessage(), e, null); } // and we store it in the database if (record != null) { if (profile != null) { record.setProfile(profile); // if the profile is null we set the level completion to complete } else { record.setInputLevelCompletion(new boolean[]{true, true, true}, new Date(System.currentTimeMillis())); } try { final long startWrite = System.currentTimeMillis(); mdWriter.writeRecord(record, false, true); writeTime = System.currentTimeMillis() - startWrite; } catch (MD_IOException e) { throw new MetadataIoException("The service has throw an SQLException while writing the metadata :" + e.getMessage(), e, null); } final long time = System.currentTimeMillis() - start; final StringBuilder report = new StringBuilder("inserted new FullRecord: "); report.append(record.getTitle()).append('[').append(record.getIdentifier()).append(']').append("( ID:").append(record.getId()); report.append(" in ").append(time).append(" ms (transformation: ").append(transTime).append(" DB write: ").append(writeTime).append(")"); LOGGER.log(logLevel, report.toString()); if (!noIndexation) { indexDocument(record); } return true; } return false; } protected void indexDocument(final FullRecord f) { //need to be override by child } /** * {@inheritDoc} */ @Override public void destroy() { classBinding.clear(); try { if (mdWriter != null) { mdWriter.close(); } classBinding.clear(); } catch (MD_IOException ex) { LOGGER.info("SQL Exception while destroying Metadata writer"); } } /** * {@inheritDoc} */ @Override public boolean deleteSupported() { return true; } /** * {@inheritDoc} */ @Override public boolean updateSupported() { return true; } /** * {@inheritDoc} */ @Override public boolean deleteMetadata(final String identifier) throws MetadataIoException { LOGGER.log(logLevel, "metadata to delete:{0}", identifier); if (identifier == null) { return false; } try { // TODO is a way more fast to know that the record exist? method isAlreadyRecordedRecord(int id) writer20 final RecordInfo f = mdWriter.getRecordInfo(identifier); if (f != null) { mdWriter.deleteRecord(f); } else { LOGGER.log(logLevel, "The metadata is not registered, nothing to delete"); return false; } } catch (MD_IOException ex) { throw new MetadataIoException("The service has throw an MD_IOException while deleting the metadata: " + ex.getMessage()); } return true; } /** * {@inheritDoc} */ @Override public boolean replaceMetadata(final String metadataID, final Node any) throws MetadataIoException { final boolean succeed = deleteMetadata(metadataID); if (!succeed) { return false; } return storeMetadata(any); } /** * {@inheritDoc} */ @Override public boolean isAlreadyUsedIdentifier(final String metadataID) throws MetadataIoException { try { return mdWriter.isAlreadyUsedIdentifier(metadataID); } catch (MD_IOException ex) { throw new MetadataIoException(ex); } } /** * Return an MDWeb path from a XPath. * * @param xpath An XPath * * @return An MDWeb path * @throws org.mdweb.io.MD_IOException * @throws org.constellation.metadata.io.MetadataIoException */ protected MixedPath getMDWPathFromXPath(String xpath) throws MD_IOException, MetadataIoException { //we remove the first '/' if (xpath.startsWith("/")) { xpath = xpath.substring(1); } String typeName = xpath.substring(0, xpath.indexOf('/')); if (typeName.contains(":")) { typeName = typeName.substring(typeName.indexOf(':') + 1); } xpath = xpath.substring(xpath.indexOf(typeName) + typeName.length() + 1); Classe type; // we look for a know metadata type if ("MD_Metadata".equals(typeName)) { mainStandard = Standard.ISO_19115; type = mdWriter.getClasse("MD_Metadata", mainStandard); } else if ("Record".equals(typeName)) { mainStandard = Standard.CSW; type = mdWriter.getClasse("Record", mainStandard); } else { throw new MetadataIoException("This metadata type is not allowed:" + typeName + "\n Allowed ones are: MD_Metadata or Record");//, INVALID_PARAMETER_VALUE); } Path p = new Path(mainStandard, type); final StringBuilder idValue = new StringBuilder(mainStandard.getName()).append(':').append(type.getName()).append(".*"); int ordinal = -1; while (xpath.indexOf('/') != -1) { //Then we get the next Property name String propertyName = xpath.substring(0, xpath.indexOf('/')); //remove namespace on propertyName final int separatorIndex = propertyName.indexOf(':'); if (separatorIndex != -1) { propertyName = propertyName.substring(separatorIndex + 1); } //we look for an ordinal ordinal = -1; if (propertyName.indexOf('[') != -1) { if (propertyName.indexOf(']') != -1) { try { final String ordinalValue = propertyName.substring(propertyName.indexOf('[') + 1, propertyName.indexOf(']')); ordinal = Integer.parseInt(ordinalValue); } catch (NumberFormatException ex) { throw new MetadataIoException("The xpath is malformed, the brackets value is not an integer"); } propertyName = propertyName.substring(0, propertyName.indexOf('[')); } else { throw new MetadataIoException("The xpath is malformed, unclosed bracket"); } } LOGGER.log(Level.FINER, "propertyName:{0} ordinal:{1}", new Object[]{propertyName, ordinal}); idValue.append(':').append(propertyName).append('.'); if (ordinal == -1) { idValue.append('*'); } else { idValue.append(ordinal); } final Property property = getProperty(type, propertyName); p = new Path(p, property); type = property.getType(); xpath = xpath.substring(xpath.indexOf('/') + 1); // remove type node if (xpath.indexOf('/') != -1) { xpath = xpath.substring(xpath.indexOf('/') + 1); } else { xpath = ""; } } if (!xpath.isEmpty()) { //we look for an ordinal if (xpath.indexOf('[') != -1) { if (xpath.indexOf(']') != -1) { try { final String ordinalValue = xpath.substring(xpath.indexOf('[') + 1, xpath.indexOf(']')); ordinal = Integer.parseInt(ordinalValue); } catch (NumberFormatException ex) { throw new MetadataIoException("The xpath is malformed, the brackets value is not an integer"); } xpath = xpath.substring(0, xpath.indexOf('[')); } else { throw new MetadataIoException("The xpath is malformed, unclosed bracket"); } } //remove namespace on propertyName final int separatorIndex = xpath.indexOf(':'); if (separatorIndex != -1) { xpath = xpath.substring(separatorIndex + 1); } idValue.append(':').append(xpath).append('.'); if (ordinal == -1) { idValue.append('*'); } else { idValue.append(ordinal); } LOGGER.log(Level.FINER, "last propertyName:{0} ordinal:{1}", new Object[]{xpath, ordinal}); final Property property = getProperty(type, xpath); p = new Path(p, property); } return new MixedPath(p, idValue.toString(), ordinal); } private Property getProperty(final Classe type, String propertyName) throws MD_IOException, MetadataIoException { // Special case for a bug in MDWeb if ("geographicElement".equals(propertyName)) { propertyName = "geographicElement2"; } Property property = type.getPropertyByName(propertyName); if (property == null) { // if the property is null we search in the sub-classes final List<Classe> subclasses = mdWriter.getSubClasses(type); for (Classe subClasse : subclasses) { property = subClasse.getPropertyByName(propertyName); if (property != null) { break; } } if (property == null) { throw new MetadataIoException("There is no property:" + propertyName + " in the class " + type.getName());//, INVALID_PARAMETER_VALUE); } } return property; } private Object unmarshallObject(final Node n) throws MetadataIoException { final MetadataType mode; switch (n.getLocalName()) { case "MD_Metadata": case "MI_Metadata": mode = MetadataType.ISO_19115; break; case "Record": mode = MetadataType.DUBLINCORE; break; case "SensorML": mode = MetadataType.SENSORML; break; case "RegistryObject": case "AdhocQuery": case "Association": case "RegistryPackage": case "Registry": case "ExtrinsicObject": case "RegistryEntry": mode = MetadataType.EBRIM; break; default: mode = MetadataType.NATIVE; break; } // TODO complete other metadata type try { final boolean replace = mode == MetadataType.ISO_19115; final Unmarshaller um = EBRIMMarshallerPool.getInstance().acquireUnmarshaller(); final MarshallWarnings warnings = new MarshallWarnings(); um.setProperty(LegacyNamespaces.APPLY_NAMESPACE_REPLACEMENTS, replace); um.setProperty(XML.TIMEZONE, tz); um.setProperty(XML.CONVERTER, warnings); final String xml = getStringFromNode(n); Object obj = um.unmarshal(new StringReader(xml)); EBRIMMarshallerPool.getInstance().recycle(um); if (obj instanceof JAXBElement) { obj = ((JAXBElement)obj).getValue(); } return obj; } catch (JAXBException | TransformerException ex) { throw new MetadataIoException(ex); } } private static String getStringFromNode(final Node n) throws TransformerException { TransformerFactory tf = TransformerFactory.newInstance(); Transformer transformer = tf.newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); StringWriter writer = new StringWriter(); transformer.transform(new DOMSource(n), new StreamResult(writer)); String output = writer.getBuffer().toString().replaceAll("\n|\r", ""); return output; } /** * Must be overridden by subClasses * * @param metadataID * @param properties * @return * @throws MetadataIoException */ @Override public boolean updateMetadata(final String metadataID, final Map<String, Object> properties) throws MetadataIoException { return false; } /** * @return the noLink */ public boolean isNoLink() { return noLink; } /** * @param noLink the noLink to set */ public void setNoLink(final boolean noLink) { this.noLink = noLink; } /** * @return the mdRecordSet */ public RecordSet getMdRecordSet() { return mdRecordSet; } /** * @return the defaultUser */ public User getDefaultUser() { return defaultUser; } protected static final class MixedPath { public Path path; public String idValue; public int ordinal; public MixedPath(Path path, String idValue, int ordinal) { this.path = path; this.idValue = idValue; this.ordinal = ordinal; } } }