1 /***********************************************************************
2  * Copyright (c) 2013-2025 General Atomics Integrated Intelligence, Inc.
3  * All rights reserved. This program and the accompanying materials
4  * are made available under the terms of the Apache License, Version 2.0
5  * which accompanies this distribution and is available at
6  * https://www.apache.org/licenses/LICENSE-2.0
7  ***********************************************************************/
8 
9 package org.locationtech.geomesa.features.exporters
10 
11 import org.geotools.api.data.Transaction
12 import org.geotools.api.feature.simple.{SimpleFeature, SimpleFeatureType}
13 import org.geotools.data.shapefile.files.ShpFiles
14 import org.geotools.data.shapefile.{ShapefileDataStore, ShapefileDataStoreFactory}
15 import org.geotools.util.URLs
16 import org.locationtech.geomesa.utils.io.WithClose
17 import org.locationtech.jts.geom.Geometry
18 
19 import java.io.File
20 import java.net.URL
21 
22 /**
23  * Exports to a shapefile, which is a collection of files
24  *
25  * @param file pointer to a .shp file (not a directory)
26  */
27 class ShapefileExporter(file: File) extends FeatureExporter {
28 
29   import scala.collection.JavaConverters._
30 
31   private val url = URLs.fileToUrl(file)
32   private var ds: ShapefileDataStore = _
33   private var mappings: Map[Int, Int] = _
34 
35   override def start(sft: SimpleFeatureType): Unit = {
36     // ensure that the parent directory exists, otherwise the data store will error out
37     Option(file.getParentFile).filterNot(_.exists()).foreach(_.mkdirs())
38     ds = new ShapefileDataStoreFactory().createDataStore(url).asInstanceOf[ShapefileDataStore]
39     ds.createSchema(sft)
40 
41     var i = -1
42     var j = 0
43     // map attributes according to the shapefile data store
44     // default geometry goes to attribute 0, other geometries are dropped
45     // byte arrays are dropped, but everything else is kept or transformed
46     // see: `org.geotools.data.shapefile.ShapefileDataStore.createDbaseHeader()`
47     val attributes = sft.getAttributeDescriptors.asScala.flatMap { d =>
48       i += 1
49       val binding = d.getType.getBinding
50       if (classOf[Geometry].isAssignableFrom(binding) || binding == classOf[Array[Byte]] ) { None } else {
51         j += 1
52         Some(i -> j)
53       }
54     }
55     mappings = attributes.toMap + (sft.indexOf(sft.getGeometryDescriptor.getLocalName) -> 0)
56   }
57 
58   override def export(features: Iterator[SimpleFeature]): Option[Long] = {
59     var count = 0L
60 
61     WithClose(ds.getFeatureWriterAppend(Transaction.AUTO_COMMIT)) { writer =>
62       features.foreach { feature =>
63         val toWrite = writer.next()
64         mappings.foreach { case (from, to) => toWrite.setAttribute(to, feature.getAttribute(from)) }
65         // copy over the user data
66         // note: shapefile doesn't support provided fid
67         toWrite.getUserData.putAll(feature.getUserData)
68         writer.write()
69         count += 1L
70       }
71     }
72 
73     Some(count)
74   }
75 
76   /**
77    * Counts the total bytes written, across the various files in this shapefile
78    *
79    * @return bytes written
80    */
81   def bytes: Long = {
82     val files = new ShpFiles(url)
83     try {
84       var sum = 0L
85       files.getFileNames.asScala.values.foreach { file =>
86         sum += URLs.urlToFile(new URL(file)).length()
87       }
88       sum
89     } finally {
90       files.dispose()
91     }
92   }
93 
94   override def close(): Unit = Option(ds).foreach(_.dispose)
95 }
Line Stmt Id Pos Tree Symbol Tests Code
31 538 1201 - 1205 Select org.locationtech.geomesa.features.exporters.ShapefileExporter.file ShapefileExporter.this.file
31 539 1186 - 1206 Apply org.geotools.util.URLs.fileToUrl org.geotools.util.URLs.fileToUrl(ShapefileExporter.this.file)
37 540 1445 - 1463 Apply java.io.File.getParentFile ShapefileExporter.this.file.getParentFile()
37 541 1475 - 1485 Apply java.io.File.exists x$1.exists()
37 542 1495 - 1505 Apply java.io.File.mkdirs x$2.mkdirs()
37 543 1438 - 1506 Apply scala.Option.foreach scala.Option.apply[java.io.File](ShapefileExporter.this.file.getParentFile()).filterNot(((x$1: java.io.File) => x$1.exists())).foreach[Boolean](((x$2: java.io.File) => x$2.mkdirs()))
38 544 1564 - 1567 Select org.locationtech.geomesa.features.exporters.ShapefileExporter.url ShapefileExporter.this.url
38 545 1516 - 1601 TypeApply scala.Any.asInstanceOf new org.geotools.data.shapefile.ShapefileDataStoreFactory().createDataStore(ShapefileExporter.this.url).asInstanceOf[org.geotools.data.shapefile.ShapefileDataStore]
38 546 1511 - 1601 Apply org.locationtech.geomesa.features.exporters.ShapefileExporter.ds_= ShapefileExporter.this.ds_=(new org.geotools.data.shapefile.ShapefileDataStoreFactory().createDataStore(ShapefileExporter.this.url).asInstanceOf[org.geotools.data.shapefile.ShapefileDataStore])
39 547 1606 - 1626 Apply org.geotools.data.shapefile.ShapefileDataStore.createSchema ShapefileExporter.this.ds.createSchema(sft)
41 548 1640 - 1642 Literal <nosymbol> -1
42 549 1655 - 1656 Literal <nosymbol> 0
47 550 1968 - 1995 Apply org.geotools.api.feature.simple.SimpleFeatureType.getAttributeDescriptors sft.getAttributeDescriptors()
47 564 2012 - 2012 TypeApply scala.collection.mutable.Buffer.canBuildFrom mutable.this.Buffer.canBuildFrom[(Int, Int)]
47 565 1968 - 2229 ApplyToImplicitArgs scala.collection.TraversableLike.flatMap scala.collection.JavaConverters.asScalaBufferConverter[org.geotools.api.feature.type.AttributeDescriptor](sft.getAttributeDescriptors()).asScala.flatMap[(Int, Int), scala.collection.mutable.Buffer[(Int, Int)]](((d: org.geotools.api.feature.type.AttributeDescriptor) => { i = i.+(1); val binding: Class[_] = d.getType().getBinding(); if (classOf[org.locationtech.jts.geom.Geometry].isAssignableFrom(binding).||(binding.==(classOf[[B]))) scala.this.Option.option2Iterable[Nothing](scala.None) else { j = j.+(1); scala.this.Option.option2Iterable[(Int, Int)](scala.Some.apply[(Int, Int)](scala.Predef.ArrowAssoc[Int](i).->[Int](j))) } }))(mutable.this.Buffer.canBuildFrom[(Int, Int)])
48 551 2025 - 2031 Apply scala.Int.+ i.+(1)
49 552 2052 - 2072 Apply org.geotools.api.feature.type.PropertyType.getBinding d.getType().getBinding()
50 553 2083 - 2100 Literal <nosymbol> classOf[org.locationtech.jts.geom.Geometry]
50 554 2130 - 2161 Apply java.lang.Object.== binding.==(classOf[[B])
50 555 2083 - 2161 Apply scala.Boolean.|| classOf[org.locationtech.jts.geom.Geometry].isAssignableFrom(binding).||(binding.==(classOf[[B]))
50 556 2166 - 2170 Select scala.None scala.None
50 557 2166 - 2170 ApplyImplicitView scala.Option.option2Iterable scala.this.Option.option2Iterable[Nothing](scala.None)
50 558 2166 - 2170 Block scala.Option.option2Iterable scala.this.Option.option2Iterable[Nothing](scala.None)
50 563 2178 - 2223 Block <nosymbol> { j = j.+(1); scala.this.Option.option2Iterable[(Int, Int)](scala.Some.apply[(Int, Int)](scala.Predef.ArrowAssoc[Int](i).->[Int](j))) }
51 559 2188 - 2194 Apply scala.Int.+ j.+(1)
52 560 2208 - 2214 Apply scala.Predef.ArrowAssoc.-> scala.Predef.ArrowAssoc[Int](i).->[Int](j)
52 561 2203 - 2215 Apply scala.Some.apply scala.Some.apply[(Int, Int)](scala.Predef.ArrowAssoc[Int](i).->[Int](j))
52 562 2203 - 2215 ApplyImplicitView scala.Option.option2Iterable scala.this.Option.option2Iterable[(Int, Int)](scala.Some.apply[(Int, Int)](scala.Predef.ArrowAssoc[Int](i).->[Int](j)))
55 566 2256 - 2256 TypeApply scala.Predef.$conforms scala.Predef.$conforms[(Int, Int)]
55 567 2265 - 2321 Apply scala.Predef.ArrowAssoc.-> scala.Predef.ArrowAssoc[Int](sft.indexOf(sft.getGeometryDescriptor().getLocalName())).->[Int](0)
55 568 2245 - 2322 Apply scala.collection.immutable.Map.+ attributes.toMap[Int, Int](scala.Predef.$conforms[(Int, Int)]).+[Int](scala.Predef.ArrowAssoc[Int](sft.indexOf(sft.getGeometryDescriptor().getLocalName())).->[Int](0))
55 569 2234 - 2322 Apply org.locationtech.geomesa.features.exporters.ShapefileExporter.mappings_= ShapefileExporter.this.mappings_=(attributes.toMap[Int, Int](scala.Predef.$conforms[(Int, Int)]).+[Int](scala.Predef.ArrowAssoc[Int](sft.indexOf(sft.getGeometryDescriptor().getLocalName())).->[Int](0)))
59 570 2419 - 2421 Literal <nosymbol> 0L
61 571 2463 - 2486 Select org.geotools.api.data.Transaction.AUTO_COMMIT org.geotools.api.data.Transaction.AUTO_COMMIT
61 572 2437 - 2487 Apply org.geotools.data.shapefile.ShapefileDataStore.getFeatureWriterAppend ShapefileExporter.this.ds.getFeatureWriterAppend(org.geotools.api.data.Transaction.AUTO_COMMIT)
61 583 2489 - 2489 Select org.locationtech.geomesa.utils.io.IsCloseableImplicits.closeableIsCloseable io.this.IsCloseable.closeableIsCloseable
61 584 2427 - 2877 ApplyToImplicitArgs org.locationtech.geomesa.utils.io.WithClose.apply org.locationtech.geomesa.utils.io.`package`.WithClose.apply[org.geotools.api.data.FeatureWriter[org.geotools.api.feature.simple.SimpleFeatureType,org.geotools.api.feature.simple.SimpleFeature], Unit](ShapefileExporter.this.ds.getFeatureWriterAppend(org.geotools.api.data.Transaction.AUTO_COMMIT))(((writer: org.geotools.api.data.FeatureWriter[org.geotools.api.feature.simple.SimpleFeatureType,org.geotools.api.feature.simple.SimpleFeature]) => features.foreach[Unit](((feature: org.geotools.api.feature.simple.SimpleFeature) => { val toWrite: org.geotools.api.feature.simple.SimpleFeature = writer.next(); ShapefileExporter.this.mappings.foreach[Unit](((x0$1: (Int, Int)) => x0$1 match { case (_1: Int, _2: Int)(Int, Int)((from @ _), (to @ _)) => toWrite.setAttribute(to, feature.getAttribute(from)) })); toWrite.getUserData().putAll(feature.getUserData()); writer.write(); count = count.+(1L) }))))(io.this.IsCloseable.closeableIsCloseable)
62 582 2507 - 2871 Apply scala.collection.Iterator.foreach features.foreach[Unit](((feature: org.geotools.api.feature.simple.SimpleFeature) => { val toWrite: org.geotools.api.feature.simple.SimpleFeature = writer.next(); ShapefileExporter.this.mappings.foreach[Unit](((x0$1: (Int, Int)) => x0$1 match { case (_1: Int, _2: Int)(Int, Int)((from @ _), (to @ _)) => toWrite.setAttribute(to, feature.getAttribute(from)) })); toWrite.getUserData().putAll(feature.getUserData()); writer.write(); count = count.+(1L) }))
63 573 2559 - 2572 Apply org.geotools.api.data.FeatureWriter.next writer.next()
64 574 2644 - 2670 Apply org.geotools.api.feature.simple.SimpleFeature.getAttribute feature.getAttribute(from)
64 575 2619 - 2671 Apply org.geotools.api.feature.simple.SimpleFeature.setAttribute toWrite.setAttribute(to, feature.getAttribute(from))
64 576 2619 - 2671 Block org.geotools.api.feature.simple.SimpleFeature.setAttribute toWrite.setAttribute(to, feature.getAttribute(from))
64 577 2581 - 2673 Apply scala.collection.IterableLike.foreach ShapefileExporter.this.mappings.foreach[Unit](((x0$1: (Int, Int)) => x0$1 match { case (_1: Int, _2: Int)(Int, Int)((from @ _), (to @ _)) => toWrite.setAttribute(to, feature.getAttribute(from)) }))
67 578 2800 - 2819 Apply org.geotools.api.feature.Property.getUserData feature.getUserData()
67 579 2773 - 2820 Apply java.util.Map.putAll toWrite.getUserData().putAll(feature.getUserData())
68 580 2829 - 2843 Apply org.geotools.api.data.FeatureWriter.write writer.write()
69 581 2852 - 2863 Apply scala.Long.+ count.+(1L)
73 585 2883 - 2894 Apply scala.Some.apply scala.Some.apply[Long](count)
82 586 3075 - 3078 Select org.locationtech.geomesa.features.exporters.ShapefileExporter.url ShapefileExporter.this.url
82 587 3062 - 3079 Apply org.geotools.data.shapefile.files.ShpFiles.<init> new org.geotools.data.shapefile.files.ShpFiles(ShapefileExporter.this.url)
83 593 3096 - 3238 Block <nosymbol> { var sum: Long = 0L; scala.collection.JavaConverters.mapAsScalaMapConverter[org.geotools.data.shapefile.files.ShpFileType, String](files.getFileNames()).asScala.values.foreach[Unit](((file: String) => sum = sum.+(org.geotools.util.URLs.urlToFile(new java.net.URL(file)).length()))); sum }
84 588 3106 - 3108 Literal <nosymbol> 0L
85 589 3115 - 3133 Apply org.geotools.data.shapefile.files.ShpFiles.getFileNames files.getFileNames()
85 592 3115 - 3228 Apply scala.collection.IterableLike.foreach scala.collection.JavaConverters.mapAsScalaMapConverter[org.geotools.data.shapefile.files.ShpFileType, String](files.getFileNames()).asScala.values.foreach[Unit](((file: String) => sum = sum.+(org.geotools.util.URLs.urlToFile(new java.net.URL(file)).length())))
86 590 3182 - 3220 Apply java.io.File.length org.geotools.util.URLs.urlToFile(new java.net.URL(file)).length()
86 591 3175 - 3220 Apply scala.Long.+ sum.+(org.geotools.util.URLs.urlToFile(new java.net.URL(file)).length())
90 594 3261 - 3276 Apply org.geotools.data.shapefile.files.ShpFiles.dispose files.dispose()
90 595 3261 - 3276 Block org.geotools.data.shapefile.files.ShpFiles.dispose files.dispose()
94 596 3326 - 3328 Select org.locationtech.geomesa.features.exporters.ShapefileExporter.ds ShapefileExporter.this.ds
94 597 3338 - 3347 Apply org.geotools.data.shapefile.ShapefileDataStore.dispose x$3.dispose()
94 598 3319 - 3348 Apply scala.Option.foreach scala.Option.apply[org.geotools.data.shapefile.ShapefileDataStore](ShapefileExporter.this.ds).foreach[Unit](((x$3: org.geotools.data.shapefile.ShapefileDataStore) => x$3.dispose()))