/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2013, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.data.mapinfo.mif; import org.geotoolkit.nio.IOUtilities; import org.opengis.util.GenericName; import org.apache.sis.storage.DataStoreException; import org.geotoolkit.data.mapinfo.ProjectionUtils; import org.geotoolkit.util.NamesExt; import org.apache.sis.referencing.CommonCRS; import org.apache.sis.internal.referencing.j2d.AffineTransform2D; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; import java.io.*; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.NullArgumentException; import org.apache.sis.util.logging.Logging; import org.apache.sis.util.Utilities; import static java.nio.file.StandardOpenOption.*; import org.apache.sis.feature.FeatureExt; import org.apache.sis.feature.builder.FeatureTypeBuilder; import org.apache.sis.internal.feature.AttributeConvention; import org.apache.sis.storage.IllegalNameException; import org.geotoolkit.data.internal.GenericNameIndex; import org.opengis.feature.Feature; import org.opengis.feature.FeatureType; import org.opengis.feature.IdentifiedType; import org.opengis.feature.PropertyType; /** * Read types of .mif file, and manage readers / writers (from mif/mid mapinfo exchange format). * * @author Alexis Manin (Geomatys) * Date : 20/02/13 */ public class MIFManager { public static final Logger LOGGER = Logging.getLogger("org.geotoolkit.data.mapinfo.mif"); /** * A pattern frequently used to find MIF categories (for words without digit). */ public static final Pattern ALPHA_PATTERN = Pattern.compile("[\\p{L}_][\\p{javaLetterOrDigit}_]*"); /** To manage accesses to file. */ private final ReadWriteLock RWLock = new ReentrantReadWriteLock(); /** * Mif file access */ private String mifName; private final URI mifPath; private Scanner mifScanner; /** Path to the MID file. */ private URI midPath; /** * Header tag values. See {@link MIFHeaderCategory} for tag description. */ private short mifVersion = 300; private final String mifCharset = "Neutral"; public char mifDelimiter = '\t'; private final ArrayList<Short> mifUnique = new ArrayList<>(); private final ArrayList<Short> mifIndex = new ArrayList<>(); private CoordinateReferenceSystem mifCRS = CommonCRS.WGS84.normalizedGeographic(); private MathTransform mifTransform = null; private int mifColumnsCount = -1; /** * The mif crs as it will be defined in final MIF file. We need it because it could be some differences between the * CRS of features added by user, and the one that will be written (Ex : written crs first axis have to be east). */ private CoordinateReferenceSystem writtenCRS = null; /** * All geometries in a MIF file must get the same CRS. This trigger will serve to know if user add multiple * geometries with different CRS, */ private boolean crsSet = false; /** * Type and data containers */ private GenericNameIndex<GenericName> names = null; private FeatureType mifBaseType = null; private final ArrayList<FeatureType> mifChildTypes = new ArrayList<>(); public MIFManager(File mifFile) throws NullArgumentException, DataStoreException, IOException, URISyntaxException { ArgumentChecks.ensureNonNull("Input file", mifFile); mifPath = mifFile.toURI(); init(); } public MIFManager(URI mifFilePath) throws NullArgumentException, DataStoreException, IOException, URISyntaxException { ArgumentChecks.ensureNonNull("Input file path", mifFilePath); mifPath = mifFilePath; init(); } /** * Basic operations needed in both constructors. */ private void init() throws DataStoreException, IOException, URISyntaxException { final String mifStr = mifPath.getPath(); int lastSeparatorIndex = mifStr.lastIndexOf(System.getProperty("file.separator")); mifName = mifStr.substring(lastSeparatorIndex+1); if (mifName.toLowerCase().endsWith(".mif")) { mifName = mifName.substring(0, mifName.length() - 4); } buildMIDPath(); } public CoordinateReferenceSystem getMifCRS() { return mifCRS; } public MathTransform getTransform() { return mifTransform; } /** * Return the different type names specified by this document. * <p/> * If the scanner did not already read them, we catch them all by parsing the file with {@link MIFManager#buildDataTypes()}. * * @return a list ({@link HashSet}) of available feature types in that document. * @throws DataStoreException if we get a problem parsing the file. */ public Set<GenericName> getTypeNames() throws DataStoreException { if (names == null) { names = new GenericNameIndex<>(); checkDataTypes(); } for (FeatureType t : mifChildTypes) { if(!names.getNames().contains(t.getName())) { names.add(t.getName(),t.getName()); } } if(names.getNames().isEmpty()) { if(mifBaseType!=null) { names.add(mifBaseType.getName(),mifBaseType.getName()); }/* else { throw new DataStoreException("No valid type can be found into this feature store."); }*/ } return names.getNames(); } /** * Try to add a new Feature type to the current store. * @param typeName The name of the type to add. * @param toAdd The type to add. * @throws DataStoreException If an unexpected error occurs while referencing given type. * @throws URISyntaxException If the URL specified at store creation is invalid. */ public void addSchema(GenericName typeName, FeatureType toAdd) throws DataStoreException, URISyntaxException { ArgumentChecks.ensureNonNull("New feature type", toAdd); /* * We'll try to get the available types from datastore. If an exception raises while this operation, the source * file is invalid, so we try to delete it before going on. */ try { getTypeNames(); } catch (Exception e) { // Try to clear files before rewriting in it. try { Path mifPathObj = IOUtilities.toPath(mifPath); Path midPathObj = IOUtilities.toPath(midPath); Files.deleteIfExists(mifPathObj); Files.deleteIfExists(midPathObj); } catch (IOException e1) { throw new DataStoreException("Unable to erase MIF and MID files.", e1); } refreshMetaModel(); } if(!toAdd.isSimple()){ throw new DataStoreException("Only Simple Features, or features with a Simple Feature as parent can be added."); } //We check for the crs first checkTypeCRS(toAdd); boolean isBaseType = false; // If we're on a new store, we must set the base type and write the header. If the source type is non-geometric, // we save it as our base type. Otherwise, we set it's super type as base type, and if there's not, we set it as // base type, but we extract geometry first. if (mifBaseType == null) { final IdentifiedType geom = MIFUtils.findGeometryProperty(toAdd); if (geom == null) { mifBaseType = toAdd; isBaseType = true; } else if (!toAdd.getSuperTypes().isEmpty()) { mifBaseType = (FeatureType) toAdd.getSuperTypes().iterator().next(); checkTypeCRS(toAdd); } else { final FeatureTypeBuilder builder = new FeatureTypeBuilder(toAdd); builder.getProperty(geom.getName().tip().toString()).remove(); builder.setName(mifName+".baseType"); mifBaseType = builder.build(); } mifColumnsCount = mifBaseType.getProperties(true).size(); flushHeader(); } // If the given type has not been added as is as base type, we try to put it into our childTypes. if(!isBaseType) { FeatureType childType = toAdd; if(!toAdd.getSuperTypes().contains(mifBaseType)) { FeatureTypeBuilder builder = new FeatureTypeBuilder(toAdd); builder.setSuperTypes(mifBaseType); childType = builder.build(); } if (MIFUtils.identifyFeature(childType) != null) { mifChildTypes.add(childType); } else { throw new DataStoreException("The geometry for the given type is not supported for MIF geometry"); } } } private void checkTypeCRS(FeatureType toCheck) throws DataStoreException { CoordinateReferenceSystem crs = FeatureExt.getCRS(toCheck); if (crs == null) return; if (!crsSet) { mifCRS = crs; crsSet = true; /** * We check if mif conversion will modify the defined CRS. If it is the case, we store the modified CRS. * This CRS will serve us as file writing, as we will have to reproject our features to fit the final system. */ if (!Utilities.equalsIgnoreMetadata(mifCRS, CommonCRS.WGS84.normalizedGeographic())) { try { final String mifCRSDefinition = ProjectionUtils.crsToMIFSyntax(mifCRS); if (mifCRSDefinition != null && !mifCRSDefinition.isEmpty()) { writtenCRS = ProjectionUtils.buildCRSFromMIF(mifCRSDefinition); if (Utilities.equalsIgnoreMetadata(mifCRS, writtenCRS)) { writtenCRS = null; } } } catch (Exception e) { // Nothing to do here, if a CRS incompatibility has been raised, it will be well raise at MIF file flushing. } } } else if (!mifCRS.equals(crs)) { throw new DataStoreException("Given type CRS is not compatible with the one previously specified." + "\nExpected : " + mifCRS + "\nFound : " + crs); } } public void deleteSchema(String typeName) throws DataStoreException { getTypeNames(); final GenericName fullName = names.get(typeName); if (fullName!=null) { if (mifBaseType.getName().equals(fullName)) { mifBaseType = null; } else { for (int i = 0 ; i < mifChildTypes.size() ; i++) { if(mifChildTypes.get(i).getName().equals(fullName)) { mifChildTypes.remove(i); break; } } } } else { throw new DataStoreException("Unable to delete the feature type named " + typeName + "because it does not exists in this data store."); } } public FeatureType getType(String typeName) throws DataStoreException { getTypeNames(); final GenericName fullName = names.get(typeName); if(mifBaseType.getName().equals(fullName)) { return mifBaseType; } if(names.getNames().contains(fullName)) { for(FeatureType t : mifChildTypes) { if(t.getName().equals(fullName)) { return t; } } } throw new DataStoreException("No type matching the given name have been found."); } public FeatureType getBaseType() throws DataStoreException { if(mifBaseType == null && mifColumnsCount <0) { parseHeader(); } return mifBaseType; } public URI getMIFPath() { return mifPath; } public URI getMIDPath() { return midPath; } /** * Initialize the path to MID file. It can set midPath if, and ONLY if the mif header have been successfully parsed. * If column count is 0, MID file won't contain any content, so we don't care about it. * * @throws DataStoreException if the MIF path is malformed (because we use it to build MID path), or if there's no * valid file at built location. */ private void buildMIDPath() throws DataStoreException, IOException, URISyntaxException { // We try to retrieve the mid path using files, so we can use filter which don't care about case. if (IOUtilities.canProcessAsPath(mifPath)) { final Path mif = IOUtilities.toPath(mifPath); final String mifName = mif.getFileName().toString(); final String midCandidate = mifName.replaceFirst("\\.(?i)mif$", "") + "\\.mid"; if (Files.exists(mif)) { DirectoryStream.Filter<Path> filter = new DirectoryStream.Filter<Path>() { @Override public boolean accept(Path entry) throws IOException { Pattern pattern = Pattern.compile(midCandidate, Pattern.CASE_INSENSITIVE); Matcher match = pattern.matcher(entry.getFileName().toString()); return match.matches() && Files.isRegularFile(entry); } }; try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(mif.getParent(), filter)) { Iterator<Path> iterator = dirStream.iterator(); if (iterator.hasNext()) { midPath = iterator.next().toUri(); } } } if (midPath == null) { midPath = IOUtilities.changeExtension(mif, "mid").toUri(); } } else { final String mifStr = mifPath.getPath(); String midStr = null; if (mifStr.endsWith(".mif") || mifStr.endsWith(".MIF")) { midStr = mifStr.substring(0, mifStr.length() - 4); } else { throw new DataStoreException("There's an extension problem with Mif file. A correct extension is needed in order to retrieve the associated mid file."); } // We have to check if the extension is upper or lower case, since unix file systems are case sensitive. if (mifStr.endsWith(".mif")) { midStr = midStr.concat(".mid"); } else if (mifStr.endsWith(".MIF")) { midStr = midStr.concat(".MID"); } midPath = URI.create(midStr); } } /********************************************** * Methods with file access **********************************************/ /** * Read .MIF file header and get needed information for data reading. * * @throws DataStoreException If all mandatory data can't be read. */ private void parseHeader() throws DataStoreException { //Reset the file scanner to ensure we'll start on file top position. InputStream mifStream = null; try { if (mifScanner != null) { mifScanner.close(); // Should we try to unlock the file at the same time we close scanner ? //RWLock.readLock().unlock(); } RWLock.readLock().lock(); mifStream = IOUtilities.open(mifPath); mifScanner = new Scanner(mifStream, MIFUtils.DEFAULT_CHARSET); // A trigger to tell us if all mandatory categories have been parsed. boolean columnsParsed = false; while (mifScanner.hasNextLine()) { final String matched = mifScanner.findInLine(ALPHA_PATTERN); if (matched == null && !columnsParsed) { // maybe we missed a line ? mifScanner.nextLine(); continue; } if (matched.equalsIgnoreCase(MIFUtils.HeaderCategory.VERSION.name())) { if (mifScanner.hasNextShort()) { mifVersion = mifScanner.nextShort(); } else { throw new DataStoreException("MIF Version can't be read."); } } else if (matched.equalsIgnoreCase(MIFUtils.HeaderCategory.CHARSET.name())) { final String charset = mifScanner.findInLine(ALPHA_PATTERN); } else if (matched.equalsIgnoreCase(MIFUtils.HeaderCategory.DELIMITER.name())) { final String tmpStr = mifScanner.findInLine("(\"|\')[^\"](\"|\')"); if (tmpStr == null || tmpStr.length() != 3) { throw new DataStoreException(MIFHeaderCategory.DELIMITER.name() + " tag value is not formatted as it should (must be \"C\" with C the wanted delimiter character)."); } mifDelimiter = (char) tmpStr.getBytes()[1]; } else if (matched.equalsIgnoreCase(MIFUtils.HeaderCategory.UNIQUE.name())) { while (mifScanner.hasNextShort()) { mifUnique.add(mifScanner.nextShort()); } } else if (matched.equalsIgnoreCase(MIFUtils.HeaderCategory.INDEX.name())) { while (mifScanner.hasNextShort()) { mifUnique.add(mifScanner.nextShort()); } } else if (matched.equalsIgnoreCase(MIFUtils.HeaderCategory.COORDSYS.name())) { /* * Don't know how many coefficients will be defined in the CRS, nor if it's written on a single * line,so we iterate until the next header category clause, storing encountered data. */ final StringBuilder crsStr = new StringBuilder(); boolean coordSysCase = true; while(coordSysCase) { crsStr.append(mifScanner.next()); for(MIFUtils.HeaderCategory category : MIFUtils.HeaderCategory.values()) { Pattern pat = Pattern.compile(category.name(), Pattern.CASE_INSENSITIVE); if(mifScanner.hasNext(pat)) { coordSysCase = false; break; } } } final CoordinateReferenceSystem crs = ProjectionUtils.buildCRSFromMIF(crsStr.toString()); if(crs != null) { mifCRS = crs; } } else if (matched.equalsIgnoreCase(MIFUtils.HeaderCategory.TRANSFORM.name())) { double xResample, yResample, xTranslate, yTranslate; xResample = mifScanner.nextDouble(); yResample = mifScanner.nextDouble(); xTranslate = mifScanner.nextDouble(); yTranslate = mifScanner.nextDouble(); mifTransform = new AffineTransform2D(xResample, 0, 0, yResample, xTranslate, yTranslate); //Build the parent feature type for data contained in this MIF. } else if (matched.equalsIgnoreCase(MIFUtils.HeaderCategory.COLUMNS.name())) { if (mifScanner.hasNextShort()) { mifColumnsCount = mifScanner.nextShort(); } else { throw new DataStoreException("MIF Columns has no attribute count specified."); } // If there's no defined column, there will not be any base type, only pure geometry features. if (mifColumnsCount > 0) { parseColumns(); } columnsParsed = true; } else if (matched.equalsIgnoreCase(MIFUtils.HeaderCategory.DATA.name())) { if (!columnsParsed) { throw new DataStoreException("File header can't be read (Columns mark is missing)"); } else { break; } } mifScanner.nextLine(); } } catch (IOException e) { LOGGER.log(Level.WARNING, "File header can't be read (creation mode ?)."); } catch (Exception e) { throw new DataStoreException("MIF file header can't be read.", e); } finally { if(mifStream != null) { try { mifStream.close(); } catch (IOException e) { LOGGER.log(Level.WARNING, "Input connection to MIF data can't be closed.", e); } } RWLock.readLock().unlock(); } } /** * Ensure that dataTypes are built. If not, call {@link MIFManager#buildDataTypes()}. * @throws DataStoreException If we cannot read header to build data types. */ public void checkDataTypes() throws DataStoreException { if (mifBaseType == null && mifColumnsCount < 0) { parseHeader(); } if (mifChildTypes.isEmpty() && mifScanner!=null) { try { mifScanner.close(); RWLock.readLock().lock(); mifScanner = new Scanner(IOUtilities.open(mifPath), MIFUtils.DEFAULT_CHARSET); buildDataTypes(); } catch (Exception e) { LOGGER.log(Level.WARNING, "Reading types from MIF file failed.", e); } finally { mifScanner.close(); RWLock.readLock().unlock(); } } } /** * Browse the MIF file to get the geometry types it contained. With it, we create a new feature type * for each geometry type found. They'll all get the base feature type (defining MID attributes) as parent. * <p/> * IMPORTANT : we'll browse the file only for geometry TYPES, so no other data is read. * <p/> * MORE IMPORTANT : This method does not manage scanner start position, we assume that caller have prepared it * itself (to avoid close / re-open each time). */ private void buildDataTypes() { mifChildTypes.clear(); ArrayList<String> triggeredTypes = new ArrayList<>(); while (mifScanner.hasNextLine()) { final String typename = mifScanner.findInLine(ALPHA_PATTERN); if (typename != null) { if (triggeredTypes.contains(typename)) { continue; } triggeredTypes.add(typename); final FeatureType bind = MIFUtils.getGeometryType(typename, mifCRS, mifBaseType); if (bind != null) { FeatureTypeBuilder builder = new FeatureTypeBuilder(bind); builder.setName(NamesExt.getNamespace(bind.getName()), mifBaseType.getName().tip().toString()+"_"+bind.getName().tip().toString()); mifChildTypes.add(builder.build()); } } mifScanner.nextLine(); } } /** * Parse Column section of MIF file header. */ private void parseColumns() throws DataStoreException { final FeatureTypeBuilder ftb = new FeatureTypeBuilder(); ftb.setName(mifName); // Check the attributes for (int i = 0; i < mifColumnsCount; i++) { mifScanner.nextLine(); final String attName = mifScanner.findInLine(ALPHA_PATTERN); final String tmpType = mifScanner.findInLine(ALPHA_PATTERN); // Since scanner doesn't move if no matching pattern is found, we can test only the second string. if (tmpType == null) { throw new DataStoreException("A problem occured while reading columns tag from .MIF header."); } final Class binding = MIFUtils.getColumnJavaType(tmpType); if (binding == null) { throw new DataStoreException( "The typename " + tmpType + "(from " + attName + " attribute) is an unknown attribute type."); } /** todo : instantiate filters for String & Double type (length limitations). */ ftb.addAttribute(binding).setName(attName); } mifBaseType = ftb.build(); } /** * delete the MIF/MID files currently pointed by this manager. * * @return true if the files have successfully been deleted, false otherwise. */ private boolean delete() throws DataStoreException { int deleteCounter = 0; RWLock.writeLock().lock(); try { File mifFile = new File(mifPath); File midFile = new File(midPath); if (mifFile.exists()) { if (mifFile.delete()) { deleteCounter++; } } else { deleteCounter++; } if (midFile.exists()) { if (midFile.delete()) { deleteCounter++; } } else { deleteCounter++; } } catch (Exception ex) { throw new DataStoreException("MIF/MID data files can't be removed.", ex); } finally { RWLock.writeLock().unlock(); } return (deleteCounter > 1); } /** * Write the MIF file header(Version, MID columns and other stuff). * * @throws DataStoreException If the current FeatureType is not fully compliant with MIF constraints. If there's a * problem while writing the featureType in MIF header. */ public String buildHeader() throws DataStoreException { final FeatureType toWorkWith = mifBaseType; int tmpCount = toWorkWith.getProperties(true).size(); final StringBuilder headBuilder = new StringBuilder(); try { headBuilder.append(MIFUtils.HeaderCategory.VERSION).append(' ').append(mifVersion).append('\n'); headBuilder.append(MIFUtils.HeaderCategory.CHARSET).append(' ').append(mifCharset).append('\n'); headBuilder.append(MIFUtils.HeaderCategory.DELIMITER).append(' ').append('\"').append(mifDelimiter).append('\"').append('\n'); if (mifCRS != null && mifCRS != CommonCRS.WGS84.normalizedGeographic()) { String strCRS = ProjectionUtils.crsToMIFSyntax(mifCRS); if(!strCRS.isEmpty()) { headBuilder.append(strCRS).append('\n'); } else { throw new DataStoreException("Given CRS can't be written in MIF file."); } } // Check the number of attributes, as the fact we've got at most one geometry. boolean geometryFound = false; for (PropertyType desc : toWorkWith.getProperties(true)) { if (AttributeConvention.isGeometryAttribute(desc)) { if (geometryFound) { throw new DataStoreException("Only mono geometry types are managed for MIF format, but given featureType get at least 2 geometry descriptor."); } else { tmpCount--; geometryFound = true; } } } headBuilder.append(MIFUtils.HeaderCategory.COLUMNS).append(' ').append(mifColumnsCount).append('\n'); MIFUtils.featureTypeToMIFSyntax(toWorkWith, headBuilder); headBuilder.append(MIFUtils.HeaderCategory.DATA).append('\n'); } catch (Exception e) { throw new DataStoreException("Datastore can't write MIF file header.", e); } // Header successfully built, we can report featureType values on datastore attributes. mifColumnsCount = tmpCount; mifBaseType = toWorkWith; return headBuilder.toString(); } private void flushHeader() throws DataStoreException { // Cache the header in memory. final String head = buildHeader(); // Writing pass, with datastore locking. OutputStreamWriter stream = null; RWLock.writeLock().lock(); try { // writing MIF header and geometries. OutputStream out = IOUtilities.openWrite(mifPath, CREATE, WRITE, TRUNCATE_EXISTING); stream = new OutputStreamWriter(out); stream.write(head); } catch (Exception e) { throw new DataStoreException("A problem have been encountered while flushing data.", e); } finally { RWLock.writeLock().unlock(); if (stream != null) { try { stream.close(); } catch (IOException e) { LOGGER.log(Level.WARNING, "Writer stream can't be closed.", e); } } } } /** * When opening MIF file in writing mode, we write all data in tmp file. This function is used for writing tmp file * data into the real file. */ public void flushData(MIFFeatureWriter dataToWrite) throws DataStoreException { // Writing pass, with datastore locking. OutputStreamWriter stream = null; InputStream mifInput = null; InputStreamReader reader; InputStream midInput = null; RWLock.writeLock().lock(); try { // writing MIF header and geometries. OutputStream out = IOUtilities.openWrite(mifPath, CREATE, WRITE, APPEND); stream = new OutputStreamWriter(out); mifInput = dataToWrite.getMIFTempStore(); MIFUtils.write(mifInput, stream); stream.close(); // MID writing out = IOUtilities.openWrite(midPath, CREATE, WRITE, APPEND); stream = new OutputStreamWriter(out); midInput = dataToWrite.getMIDTempStore(); MIFUtils.write(midInput, stream); } catch (Exception e) { throw new DataStoreException("A problem have been encountered while flushing data.", e); } finally { RWLock.writeLock().unlock(); if (stream != null) { try { stream.close(); } catch (IOException e) { LOGGER.log(Level.WARNING, "Writer stream can't be closed.", e); } } if (mifInput != null) { try { mifInput.close(); } catch (IOException e) { LOGGER.log(Level.WARNING, "Temporary data store can't be closed.", e); } } if (midInput != null) { try { midInput.close(); } catch (IOException e) { LOGGER.log(Level.WARNING, "Temporary data store can't be closed.", e); } } } } /** * Build a string representation of the given feature attributes for MID file writing. * @param toParse The feature to convert into MID syntax. * @return A string representation of the given feature. Never null, but empty string is possible. */ public String buildMIDAttributes(Feature toParse) { final StringBuilder builder = new StringBuilder(); final FeatureType fType = toParse.getType(); if(mifBaseType.isAssignableFrom(fType)) { int i=0; for(PropertyType pt : mifBaseType.getProperties(true)){ if(AttributeConvention.contains(pt.getName())) continue; if(i!=0) builder.append(mifDelimiter); builder.append(MIFUtils.getStringValue(toParse.getPropertyValue(pt.getName().toString()))); i++; } builder.append('\n'); } return builder.toString(); } /** * {@inheritDoc} */ public void refreshMetaModel() throws IllegalNameException { names.clear(); names = null; mifBaseType = null; mifChildTypes.clear(); mifColumnsCount = -1; } public void setDelimiter(char delimiter) { this.mifDelimiter = delimiter; } public CoordinateReferenceSystem getWrittenCRS() { return writtenCRS; } }