/* * 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.filesystem; import org.constellation.generic.database.Automatic; import org.constellation.metadata.io.AbstractMetadataWriter; import org.constellation.metadata.io.MetadataIoException; import org.constellation.metadata.io.filesystem.sql.MetadataDatasource; import org.constellation.metadata.io.filesystem.sql.Session; import org.constellation.metadata.utils.Utils; import org.constellation.util.NodeUtilities; import org.geotoolkit.lucene.index.AbstractIndexer; import org.w3c.dom.Comment; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; 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.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.StringReader; import java.nio.file.Files; import java.sql.SQLException; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Level; import javax.inject.Inject; import javax.xml.stream.XMLInputFactory; import org.constellation.admin.SpringHelper; import org.constellation.business.IMetadataBusiness; import org.constellation.configuration.ConfigurationException; import static org.geotoolkit.ows.xml.OWSExceptionCode.INVALID_PARAMETER_VALUE; import static org.geotoolkit.ows.xml.OWSExceptionCode.NO_APPLICABLE_CODE; import org.xml.sax.InputSource; // JAXB dependencies //geotoolkit dependencies // Constellation dependencies /** * A CSW Metadata Writer. This writer does not require a database. * The CSW records are stored XML file in a directory . * * @author Guilhem Legal (Geomatys) */ public class FileMetadataWriter extends AbstractMetadataWriter { /** * An indexer lucene to add object into the index. */ protected final AbstractIndexer indexer; /** * A directory in witch the metadata files are stored. */ protected final File dataDirectory; protected final String serviceID; @Inject protected IMetadataBusiness metadataBusiness; protected final DocumentBuilderFactory dbf; protected final XMLInputFactory xif = XMLInputFactory.newFactory(); /** * Build a new File metadata writer, with the specified indexer. * * @param indexer A lucene indexer. * @param configuration An object containing all the dataSource informations (in this case the data directory). * * @throws org.constellation.metadata.io.MetadataIoException */ public FileMetadataWriter(final Automatic configuration, final AbstractIndexer indexer, final String serviceID) throws MetadataIoException { SpringHelper.injectDependencies(this); this.indexer = indexer; this.serviceID = serviceID; dataDirectory = configuration.getDataDirectory(); if (dataDirectory == null || !dataDirectory.isDirectory()) { throw new MetadataIoException("Unable to find the data directory", NO_APPLICABLE_CODE); } dbf = DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(true); } /** * {@inheritDoc} */ @Override public boolean storeMetadata(final Node original) throws MetadataIoException { try { final String identifier = Utils.findIdentifier(original); final File f; // for windows we avoid to create file with ':' if (System.getProperty("os.name", "").startsWith("Windows")) { final String windowsIdentifier = identifier.replace(':', '-'); f = new File(dataDirectory, windowsIdentifier + ".xml"); } else { f = new File(dataDirectory, identifier + ".xml"); } TransformerFactory tf = TransformerFactory.newInstance(); Transformer transformer = tf.newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); FileOutputStream fos = new FileOutputStream(f); StreamResult sr = new StreamResult(new OutputStreamWriter(fos, "UTF-8")); transformer.transform(new DOMSource(original),sr); if (indexer != null) { indexer.removeDocument(identifier); indexer.indexDocument(original); } Session session = null; try { session = MetadataDatasource.createSession(serviceID); if (!session.existRecord(identifier)) { session.putRecord(identifier, f.getPath()); } else { session.updateRecord(identifier, f.getPath()); } } catch (SQLException ex) { throw new MetadataIoException("SQL Exception while reading path for record", ex, NO_APPLICABLE_CODE); } finally { if (session != null) { session.close(); } } } catch (IOException | TransformerException ex) { throw new MetadataIoException("Unable to write the file.", ex, NO_APPLICABLE_CODE); } return true; } /** * {@inheritDoc} */ @Override public boolean deleteSupported() { return true; } /** * {@inheritDoc} */ @Override public boolean updateSupported() { return true; } /** * {@inheritDoc} */ @Override public boolean deleteMetadata(final String metadataID) throws MetadataIoException { final File metadataFile = getFileFromIdentifier(metadataID); if (metadataFile.exists()) { boolean suceed = false; try{ // see http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4715154 System.gc(); suceed = Files.deleteIfExists(metadataFile.toPath()); } catch (IOException ex) { LOGGER.warning("IO exception while deleting file: " + ex.getMessage()); } if (suceed) { if (indexer != null) { indexer.removeDocument(metadataID); } Session session = null; try { session = MetadataDatasource.createSession(serviceID); session.removeRecord(metadataID); } catch (SQLException ex) { throw new MetadataIoException("SQL Exception while reading path for record", ex, NO_APPLICABLE_CODE); } finally { if (session != null) { session.close(); } } } else { LOGGER.warning("unable to delete the matadata file"); } return suceed; } else { throw new MetadataIoException("The metadataFile : " + metadataID + ".xml is not present", INVALID_PARAMETER_VALUE); } } /** * {@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); } @Override public boolean isAlreadyUsedIdentifier(String metadataID) throws MetadataIoException { return getFileFromIdentifier(metadataID) != null; } /** * {@inheritDoc} */ @Override public boolean updateMetadata(final String metadataID, final Map<String , Object> properties) throws MetadataIoException { final Document metadataDoc = getDocumentFromFile(metadataID); for (Entry<String, Object> property : properties.entrySet()) { String xpath = property.getKey(); // we remove the first / before the type declaration if (xpath.startsWith("/")) { xpath = xpath.substring(1); } if (xpath.indexOf('/') != -1) { //we get the type of the metadata (first part of the Xpath String typeName = xpath.substring(0, xpath.indexOf('/')); if (typeName.contains(":")) { typeName = typeName.substring(typeName.indexOf(':') + 1); } Node parent = metadataDoc.getDocumentElement(); // we verify that the metadata to update has the same type that the Xpath type if (!parent.getLocalName().equals(typeName)) { throw new MetadataIoException("The metadata :" + metadataID + " is not of the same type that the one describe in Xpath expression", INVALID_PARAMETER_VALUE); } //we remove the type name from the xpath xpath = xpath.substring(xpath.indexOf('/') + 1); List<Node> nodes = Arrays.asList(parent); while (xpath.indexOf('/') != -1) { //Then we get the next Property name String propertyName = xpath.substring(0, xpath.indexOf('/')); final int ordinal = NodeUtilities.extractOrdinal(propertyName); final int braceIndex = propertyName.indexOf('['); if (braceIndex != -1) { propertyName = propertyName.substring(0,braceIndex); } //remove namespace on propertyName final int separatorIndex = propertyName.indexOf(':'); if (separatorIndex != -1) { propertyName = propertyName.substring(separatorIndex + 1); } nodes = NodeUtilities.getNodes(propertyName, nodes, ordinal, true); xpath = xpath.substring(xpath.indexOf('/') + 1); } // we update the metadata final Node value = (Node) property.getValue(); //remove namespace on propertyName final int separatorIndex = xpath.indexOf(':'); if (separatorIndex != -1) { xpath = xpath.substring(separatorIndex + 1); } updateObjects(nodes, xpath, value); // we finish by updating the metadata. deleteMetadata(metadataID); storeMetadata(metadataDoc.getDocumentElement()); return true; } } return false; } /** * Update an object by calling the setter of the specified property with the specified value. * * @param nodes The parent object on witch call the setters. * @param propertyName The name of the property to update on the parent (can contain an ordinal). * @param value The new value to update. * * @throws org.constellation.ws.MetadataIoException */ private void updateObjects(List<Node> nodes, String propertyName, Node value) throws MetadataIoException { Class parameterType = value.getClass(); LOGGER.log(Level.FINER, "parameter type:{0}", parameterType); final String fullPropertyName = propertyName; final int ordinal = NodeUtilities.extractOrdinal(propertyName); if (propertyName.indexOf('[') != -1) { propertyName = propertyName.substring(0, propertyName.indexOf('[')); } for (Node e : nodes) { final List<Node> toUpdate = NodeUtilities.getChilds(e, propertyName); // ADD if (toUpdate.isEmpty()) { final Node newNode = e.getOwnerDocument().createElementNS("TODO", propertyName); final Node clone = e.getOwnerDocument().importNode(value, true); newNode.appendChild(clone); e.appendChild(newNode); // UPDATE } else { for (int i = 0; i < toUpdate.size(); i++) { if (ordinal == -1 || i == ordinal) { Node n = toUpdate.get(i); final Node firstChild = getFirstChild(n, value instanceof Text); if (firstChild != null) { final Node clone = n.getOwnerDocument().importNode(value, true); n.replaceChild(clone, firstChild); } } } } } } private Document getDocumentFromFile(String identifier) throws MetadataIoException { final File metadataFile = getFileFromIdentifier(identifier); if (metadataFile.exists()) { try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(true); DocumentBuilder docBuilder = dbf.newDocumentBuilder(); Document document = docBuilder.parse(metadataFile); return document; } catch (SAXException | IOException | ParserConfigurationException ex) { throw new MetadataIoException("The metadataFile : " + identifier + ".xml can not be read\ncause: " + ex.getMessage(), ex, INVALID_PARAMETER_VALUE); } } else { throw new MetadataIoException("The metadataFile : " + identifier + ".xml is not present", INVALID_PARAMETER_VALUE); } } private Node getFirstChild(final Node n, final boolean isText) { final NodeList nl = n.getChildNodes(); for (int i = 0; i < nl.getLength(); i++) { final Node child = nl.item(i); if (isText || (!(child instanceof Text) && !(child instanceof Comment))) { return child; } } return null; } private File getFileFromIdentifier(final String identifier) throws MetadataIoException { Session session = null; try { session = MetadataDatasource.createSession(serviceID); final String path = session.getPathForRecord(identifier); return new File(path); } catch (SQLException ex) { throw new MetadataIoException("SQL Exception while reading path for record", ex, NO_APPLICABLE_CODE); } finally { if (session != null) { session.close(); } } } /** * Destoy all the resource and close connection. */ @Override public void destroy() { if (indexer != null) { indexer.destroy(); } MetadataDatasource.close(serviceID); } @Override public boolean canImportInternalData() { return true; } @Override public void linkInternalMetadata(String metadataID) throws MetadataIoException { final String xml = metadataBusiness.searchMetadata(metadataID, true, false); if (xml != null) { try { final InputSource source = new InputSource(new StringReader(xml)); final DocumentBuilder docBuilder = dbf.newDocumentBuilder(); final Document document = docBuilder.parse(source); final Node n = document.getDocumentElement(); storeMetadata(n); metadataBusiness.linkMetadataIDToCSW(metadataID, metadataID); } catch (SAXException | IOException | ParserConfigurationException | ConfigurationException ex) { throw new MetadataIoException(ex); } } } }