Skip to content

Commit 88c5482

Browse files
KJoslynjsarabia
authored andcommitted
Importing Resource Container from Zip File (#423)
* Refactor to support loading of zip RC's. No Tree support yet. * Creation of OtterTree from zip file works, several issues remain 1. Collection slugs are incorrect 2. chapters, verses, and body/notes are being put into the tree backwards * Polished and moved ZipEntryTreeBuilder to new package * Update CollectionRepository.kt * Two menu options to import from zip or folder * PR Comment * Update travis.yml to clone dev branch of kotlin-resource-container instead of master * Use ResourceContainer's new autoclose feature. * Corrected mispelled package name * PR Comment- categories and versification * PR Comments- mispelling and categories and versification
1 parent 869e495 commit 88c5482

File tree

8 files changed

+152
-60
lines changed

8 files changed

+152
-60
lines changed

.travis.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ cache:
99
- $HOME/.gradle/caches/
1010
- $HOME/.gradle/wrapper
1111
install:
12-
- git clone --branch=master https://github.com/WycliffeAssociates/kotlin-resource-container.git ../kotlin-resource-container
12+
- git clone --branch=dev https://github.com/WycliffeAssociates/kotlin-resource-container.git ../kotlin-resource-container
1313
- git clone https://github.com/WycliffeAssociates/8woc2018-common.git ../8woc2018-common
1414
- export BRANCH=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then echo $TRAVIS_BRANCH; else echo $TRAVIS_PULL_REQUEST_BRANCH; fi)
1515
- echo "TRAVIS_BRANCH=$TRAVIS_BRANCH, PR=$PR, BRANCH=$BRANCH"

src/main/kotlin/org/wycliffeassociates/otter/jvm/app/ui/inject/Injector.kt

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.wycliffeassociates.otter.jvm.app.ui.inject
22

33
import org.wycliffeassociates.otter.jvm.device.audio.injection.DaggerAudioComponent
44
import org.wycliffeassociates.otter.jvm.device.audioplugin.injection.DaggerAudioPluginComponent
5+
import org.wycliffeassociates.otter.jvm.domain.resourcecontainer.project.ZipEntryTreeBuilder
56
import org.wycliffeassociates.otter.jvm.persistence.injection.DaggerPersistenceComponent
67
import org.wycliffeassociates.otter.jvm.persistence.repositories.*
78
import org.wycliffeassociates.otter.jvm.persistence.repositories.mapping.LanguageMapper
@@ -36,4 +37,6 @@ class Injector : Component(), ScopedInstance {
3637
get() = audioComponent.injectPlayer()
3738

3839
val audioPluginRegistrar = audioPluginComponent.injectRegistrar()
40+
41+
val zipEntryTreeBuilder = ZipEntryTreeBuilder
3942
}

src/main/kotlin/org/wycliffeassociates/otter/jvm/app/ui/menu/view/MainMenu.kt

+34-18
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package org.wycliffeassociates.otter.jvm.app.ui.menu.view
22

33
import com.github.thomasnield.rxkotlinfx.toObservable
4-
import de.jensd.fx.glyphs.materialicons.MaterialIcon
5-
import de.jensd.fx.glyphs.materialicons.MaterialIconView
64
import javafx.application.Platform
75
import javafx.event.EventHandler
6+
import javafx.scene.control.Menu
87
import javafx.scene.control.MenuBar
8+
import javafx.scene.control.MenuItem
99
import javafx.scene.control.ToggleGroup
10+
import javafx.stage.FileChooser
1011
import org.wycliffeassociates.otter.jvm.app.theme.AppStyles
1112
import org.wycliffeassociates.otter.jvm.app.ui.addplugin.view.AddPluginView
1213
import org.wycliffeassociates.otter.jvm.app.ui.menu.viewmodel.MainMenuViewModel
@@ -19,27 +20,42 @@ class MainMenu : MenuBar() {
1920

2021
private val viewModel: MainMenuViewModel = find()
2122

23+
private fun Menu.importMenuItem(message: String): MenuItem {
24+
return item(message) {
25+
graphic = MainMenuStyles.importIcon("20px")
26+
val dialog = progressdialog {
27+
text = message
28+
graphic = MainMenuStyles.importIcon("60px")
29+
root.addClass(AppStyles.progressDialog)
30+
}
31+
viewModel.showImportDialogProperty.onChange {
32+
Platform.runLater { if (it) dialog.open() else dialog.close() }
33+
}
34+
}
35+
}
36+
2237
init {
2338
importStylesheet<MainMenuStyles>()
2439
with(this) {
2540
menu(messages["file"]) {
26-
item(messages["importResource"]) {
27-
graphic = MainMenuStyles.importIcon("20px")
28-
val dialog = progressdialog {
29-
text = messages["importResource"]
30-
graphic = MainMenuStyles.importIcon( "60px")
31-
root.addClass(AppStyles.progressDialog)
32-
}
33-
viewModel.showImportDialogProperty.onChange {
34-
Platform.runLater { if (it) dialog.open() else dialog.close() }
35-
}
36-
action {
37-
val file = chooseDirectory(messages["importResourceTip"])
38-
file?.let {
39-
viewModel.importContainerDirectory(file)
41+
importMenuItem(messages["importResourceFromFolder"])
42+
.setOnAction {
43+
val file = chooseDirectory(messages["importResourceFromFolder"])
44+
file?.let {
45+
viewModel.importContainerDirectory(file)
46+
}
47+
}
48+
importMenuItem(messages["importResourceFromZip"])
49+
.setOnAction {
50+
val file = chooseFile(
51+
messages["importResourceFromZip"],
52+
arrayOf(FileChooser.ExtensionFilter("Zip files (*.zip)", "*.zip")),
53+
FileChooserMode.Single
54+
).firstOrNull()
55+
file?.let {
56+
viewModel.importContainerDirectory(file)
57+
}
4058
}
41-
}
42-
}
4359
}
4460
menu(messages["audioPlugins"]) {
4561
onShowing = EventHandler {

src/main/kotlin/org/wycliffeassociates/otter/jvm/app/ui/menu/viewmodel/MainMenuViewModel.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class MainMenuViewModel : ViewModel() {
1919
private val resourceContainerRepository = injector.resourceContainerRepository
2020
private val directoryProvider = injector.directoryProvider
2121
private val pluginRepository = injector.pluginRepository
22+
private val zipEntryTreeBuilder = injector.zipEntryTreeBuilder
2223

2324
val editorPlugins: ObservableList<AudioPluginData> = FXCollections.observableArrayList<AudioPluginData>()
2425
val recorderPlugins: ObservableList<AudioPluginData> = FXCollections.observableArrayList<AudioPluginData>()
@@ -34,7 +35,8 @@ class MainMenuViewModel : ViewModel() {
3435
fun importContainerDirectory(dir: File) {
3536
val importer = ImportResourceContainer(
3637
resourceContainerRepository,
37-
directoryProvider
38+
directoryProvider,
39+
zipEntryTreeBuilder
3840
)
3941
showImportDialogProperty.value = true
4042
importer.import(dir)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.wycliffeassociates.otter.jvm.domain.resourcecontainer.project
2+
3+
import org.wycliffeassociates.otter.common.collections.tree.OtterTree
4+
import org.wycliffeassociates.otter.common.collections.tree.OtterTreeNode
5+
import org.wycliffeassociates.otter.common.domain.resourcecontainer.project.IZipEntryTreeBuilder
6+
import org.wycliffeassociates.otter.common.domain.resourcecontainer.project.OtterFile
7+
import org.wycliffeassociates.otter.common.domain.resourcecontainer.project.OtterZipFile.Companion.otterFileZ
8+
import java.io.IOException
9+
import java.nio.file.attribute.BasicFileAttributes
10+
import java.nio.file.Files
11+
import java.nio.file.SimpleFileVisitor
12+
import java.nio.file.FileSystem
13+
import java.nio.file.Paths
14+
import java.nio.file.Path
15+
import java.nio.file.FileSystems
16+
import java.nio.file.FileVisitResult
17+
import java.util.zip.ZipFile
18+
import java.util.ArrayDeque
19+
20+
object ZipEntryTreeBuilder : IZipEntryTreeBuilder {
21+
22+
private fun createZipFileSystem(zipFilename: String): FileSystem {
23+
val path = Paths.get(zipFilename)
24+
return FileSystems.newFileSystem(path, null)
25+
}
26+
27+
override fun buildOtterFileTree(zipFile: ZipFile, projectPath: String): OtterTree<OtterFile> {
28+
var treeRoot: OtterTree<OtterFile>? = null
29+
val treeCursor = ArrayDeque<OtterTree<OtterFile>>()
30+
createZipFileSystem(zipFile.name).use { zipFileSystem ->
31+
32+
val projectRoot = zipFileSystem.getPath(projectPath)
33+
val sep = zipFileSystem.separator
34+
35+
Files.walkFileTree(projectRoot, object : SimpleFileVisitor<Path>() {
36+
@Throws(IOException::class)
37+
override fun visitFile(file: Path,
38+
attrs: BasicFileAttributes): FileVisitResult {
39+
val filepath = file.toString().substringAfter(sep)
40+
val entry = zipFile.getEntry(filepath)
41+
val otterZipFile = otterFileZ(filepath, zipFile, sep, treeCursor.peek()?.value, entry)
42+
treeCursor.peek()?.addChild(OtterTreeNode(otterZipFile))
43+
return FileVisitResult.CONTINUE
44+
}
45+
46+
@Throws(IOException::class)
47+
override fun postVisitDirectory(dir: Path?, exc: IOException?): FileVisitResult {
48+
treeRoot = treeCursor.pop()
49+
return FileVisitResult.CONTINUE
50+
}
51+
52+
@Throws(IOException::class)
53+
override fun preVisitDirectory(dir: Path,
54+
attrs: BasicFileAttributes): FileVisitResult {
55+
val newDirNode = OtterTree(
56+
otterFileZ(dir.toString(), zipFile, zipFileSystem.separator, treeCursor.peek()?.value)
57+
)
58+
treeCursor.peek()?.addChild(newDirNode)
59+
treeCursor.push(newDirNode)
60+
return FileVisitResult.CONTINUE
61+
}
62+
})
63+
return treeRoot ?: OtterTree(otterFileZ(zipFile.name, zipFile, zipFileSystem.separator))
64+
}
65+
}
66+
}
67+

src/main/kotlin/org/wycliffeassociates/otter/jvm/persistence/repositories/CollectionRepository.kt

+41-39
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,17 @@ class CollectionRepository(
5858
// 2. Load the resource container
5959
val metadata = project.resourceContainer
6060
if (metadata != null) {
61-
val container = ResourceContainer.load(metadata.path)
62-
// 3. Remove the project from the manifest
63-
container.manifest.projects = container.manifest.projects.filter { it.identifier != project.slug }
64-
// 4a. If the manifest has more projects, write out the new manifest
65-
if (container.manifest.projects.isNotEmpty()) {
66-
container.writeManifest()
67-
} else {
68-
// 4b. If the manifest has no projects left, delete the RC folder and the metadata from the database
69-
metadata.path.deleteRecursively()
70-
metadataDao.delete(metadataMapper.mapToEntity(metadata))
61+
ResourceContainer.load(metadata.path).use { container ->
62+
// 3. Remove the project from the manifest
63+
container.manifest.projects = container.manifest.projects.filter { it.identifier != project.slug }
64+
// 4a. If the manifest has more projects, write out the new manifest
65+
if (container.manifest.projects.isNotEmpty()) {
66+
container.writeManifest()
67+
} else {
68+
// 4b. If the manifest has no projects left, delete the RC folder and the metadata from the database
69+
metadata.path.deleteRecursively()
70+
metadataDao.delete(metadataMapper.mapToEntity(metadata))
71+
}
7172
}
7273
}
7374
}.andThen(
@@ -212,6 +213,7 @@ class CollectionRepository(
212213
dublinCore.mapToMetadata(File("."), targetLanguage),
213214
metadata
214215
)
216+
// TODO 2/14/19: Move create to ResourceContainer to be able to create a zip resource container?
215217
val container = ResourceContainer.create(directory) {
216218
// Set up the manifest
217219
manifest = Manifest(
@@ -240,16 +242,17 @@ class CollectionRepository(
240242

241243
val metadataEntity = if (matches.isEmpty()) {
242244
// This combination of identifier and language does not already exist; create it
243-
val container = createResourceContainer(source, language)
244-
// Convert DublinCore to ResourceMetadata
245-
val metadata = container.manifest.dublinCore
246-
.mapToMetadata(container.dir, language)
245+
createResourceContainer(source, language).use { container ->
246+
// Convert DublinCore to ResourceMetadata
247+
val metadata = container.manifest.dublinCore
248+
.mapToMetadata(container.file, language)
247249

248-
// Insert ResourceMetadata into database
249-
val entity = metadataMapper.mapToEntity(metadata)
250-
entity.derivedFromFk = source.resourceContainer?.id
251-
entity.id = metadataDao.insert(entity, dsl)
252-
/* return@if */ entity
250+
// Insert ResourceMetadata into database
251+
val entity = metadataMapper.mapToEntity(metadata)
252+
entity.derivedFromFk = source.resourceContainer?.id
253+
entity.id = metadataDao.insert(entity, dsl)
254+
/* return@if */ entity
255+
}
253256
} else {
254257
// Use the existing metadata
255258
/* return@if */ matches.first()
@@ -273,27 +276,26 @@ class CollectionRepository(
273276

274277
// Add a project to the container if necessary
275278
// Load the existing resource container and see if we need to add another project
276-
val container = ResourceContainer.load(File(metadataEntity.path))
277-
if (container.manifest.projects.none { it.identifier == source.slug }) {
278-
container.manifest.projects = container.manifest.projects.plus(
279-
project {
280-
sort = if (metadataEntity.subject.toLowerCase() == "bible"
281-
&& projectEntity.sort > 39) {
282-
projectEntity.sort + 1
283-
} else {
284-
projectEntity.sort
279+
ResourceContainer.load(File(metadataEntity.path)).use { container ->
280+
if (container.manifest.projects.none { it.identifier == source.slug }) {
281+
container.manifest.projects = container.manifest.projects.plus(
282+
project {
283+
sort = if (metadataEntity.subject.toLowerCase() == "bible"
284+
&& projectEntity.sort > 39) {
285+
projectEntity.sort + 1
286+
} else {
287+
projectEntity.sort
288+
}
289+
identifier = projectEntity.slug
290+
path = "./${projectEntity.slug}"
291+
// This title will not be localized into the target language
292+
title = projectEntity.title
293+
// Unable to get categories and versification from the source collection
285294
}
286-
identifier = projectEntity.slug
287-
path = "./${projectEntity.slug}"
288-
// This title will not be localized into the target language
289-
title = projectEntity.title
290-
// Unable to get these fields from the source collection
291-
categories = listOf()
292-
versification = ""
293-
}
294-
)
295-
// Update the container
296-
container.write()
295+
)
296+
// Update the container
297+
container.write()
298+
}
297299
}
298300
}
299301
}

src/main/kotlin/org/wycliffeassociates/otter/jvm/persistence/repositories/ResourceContainerRepository.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class ResourceContainerRepository(
3737
return Completable.fromAction {
3838
database.transaction { dsl ->
3939
val language = LanguageMapper().mapFromEntity(languageDao.fetchBySlug(languageSlug, dsl))
40-
val metadata = dublinCore.mapToMetadata(rc.dir, language)
40+
val metadata = dublinCore.mapToMetadata(rc.file, language)
4141
val dublinCoreFk = resourceMetadataDao.insert(ResourceMetadataMapper().mapToEntity(metadata), dsl)
4242

4343
val relatedDublinCoreIds: List<Int> =

src/main/resources/Messages_en.properties

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ finish=FINISH
2525
refresh = Refresh
2626
file = File
2727
importResource = Import Resource Container
28+
importResourceFromFolder = Import Resource Container from Folder
29+
importResourceFromZip = Import Resource Container from Zip File
2830
importResourceTip = Please Select Resource Container to Import
2931
importError = Unable to Import Resource Container
3032
importErrorInvalidRc = Invalid resource container

0 commit comments

Comments
 (0)