diff --git a/libresonic-main/pom.xml b/libresonic-main/pom.xml
index 0d0a6234..c708318b 100644
--- a/libresonic-main/pom.xml
+++ b/libresonic-main/pom.xml
@@ -26,6 +26,15 @@
${project.version}
+
+
+ io.dropwizard.metrics
+ metrics-core
+ ${metrics.version}
+ test
+
+
+
org.springframework
spring-webmvc
diff --git a/libresonic-main/src/main/java/org/libresonic/player/service/MediaScannerService.java b/libresonic-main/src/main/java/org/libresonic/player/service/MediaScannerService.java
index 9b81e11e..445e79b5 100644
--- a/libresonic-main/src/main/java/org/libresonic/player/service/MediaScannerService.java
+++ b/libresonic-main/src/main/java/org/libresonic/player/service/MediaScannerService.java
@@ -70,6 +70,11 @@ public class MediaScannerService {
schedule();
}
+ public void initNoSchedule() {
+ deleteOldIndexFiles();
+ statistics = settingsService.getMediaLibraryStatistics();
+ }
+
/**
* Schedule background execution of media library scanning.
*/
diff --git a/libresonic-main/src/test/java/org/libresonic/player/TestCaseUtils.java b/libresonic-main/src/test/java/org/libresonic/player/TestCaseUtils.java
new file mode 100644
index 00000000..7ef165a0
--- /dev/null
+++ b/libresonic-main/src/test/java/org/libresonic/player/TestCaseUtils.java
@@ -0,0 +1,93 @@
+package org.libresonic.player;
+
+import org.libresonic.player.dao.DaoHelper;
+import org.libresonic.player.service.MediaScannerService;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class TestCaseUtils {
+
+ /**
+ * Constructs a map of records count per table.
+ *
+ * @param daoHelper DaoHelper object
+ * @return Map table name -> records count
+ */
+ public static Map recordsInAllTables(DaoHelper daoHelper) {
+ List tableNames = daoHelper.getJdbcTemplate().queryForList("" +
+ "select table_name " +
+ "from information_schema.system_tables " +
+ "where table_name not like 'SYSTEM%'"
+ , String.class);
+ Map nbRecords =
+ tableNames.stream()
+ .collect(Collectors.toMap(table -> table, table -> recordsInTable(table,daoHelper)));
+
+ return nbRecords;
+ }
+
+ /**
+ * Counts records in a table.
+ *
+ * @param tableName
+ * @param daoHelper
+ * @return
+ */
+ public static Integer recordsInTable(String tableName, DaoHelper daoHelper) {
+ return daoHelper.getJdbcTemplate().queryForInt("select count(1) from " + tableName);
+ }
+
+
+ /**
+ * Constructs the path of a resource according to the path of the current class.
+ *
+ * @param baseResources
+ * @return
+ */
+ private static String basePath(String baseResources) {
+ String basePath = TestCaseUtils.class.getResource(baseResources).toString();
+ if (basePath.startsWith("file:")) {
+ return TestCaseUtils.class.getResource(baseResources).toString().replace("file:","");
+ }
+ return basePath;
+ }
+
+
+ public static void setLibresonicHome(String baseResources) {
+ String subsoncicHome = basePath(baseResources);
+ System.setProperty("libresonic.home",subsoncicHome);
+ }
+
+ public static ApplicationContext loadSpringApplicationContext(String baseResources) {
+ String applicationContextService = baseResources + "applicationContext-service.xml";
+ String applicationContextCache = baseResources + "applicationContext-cache.xml";
+
+ String[] configLocations = new String[]{
+ TestCaseUtils.class.getClass().getResource(applicationContextCache).toString(),
+ TestCaseUtils.class.getClass().getResource(applicationContextService).toString()
+ };
+ return new ClassPathXmlApplicationContext(configLocations);
+ }
+
+
+ /**
+ * Scans the music library * @param mediaScannerService
+ */
+ public static void execScan(MediaScannerService mediaScannerService) {
+ mediaScannerService.scanLibrary();
+
+ while (mediaScannerService.isScanning()) {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+}
diff --git a/libresonic-main/src/test/java/org/libresonic/player/dao/MusicFolderDaoMock.java b/libresonic-main/src/test/java/org/libresonic/player/dao/MusicFolderDaoMock.java
new file mode 100644
index 00000000..f4127d44
--- /dev/null
+++ b/libresonic-main/src/test/java/org/libresonic/player/dao/MusicFolderDaoMock.java
@@ -0,0 +1,39 @@
+package org.libresonic.player.dao;
+
+import org.libresonic.player.domain.MusicFolder;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+public class MusicFolderDaoMock extends MusicFolderDao {
+
+ private static String baseResources = "/MEDIAS/";
+
+ public static String resolveBaseMediaPath() {
+ String baseDir = MusicFolderDaoMock.class.getResource(baseResources).toString().replace("file:","");
+ return baseDir;
+ }
+
+ public static String resolveMusicFolderPath() {
+ return (MusicFolderDaoMock.resolveBaseMediaPath() + "Music");
+ }
+
+ public static String resolveMusic2FolderPath() {
+ return (MusicFolderDaoMock.resolveBaseMediaPath() + "Music2");
+ }
+
+ @Override
+ public List getAllMusicFolders() {
+ List liste = new ArrayList<>();
+ File musicDir = new File(MusicFolderDaoMock.resolveMusicFolderPath());
+ MusicFolder musicFolder = new MusicFolder(1,musicDir,"Music",true,new Date());
+ liste.add(musicFolder);
+
+ File music2Dir = new File(MusicFolderDaoMock.resolveMusic2FolderPath());
+ MusicFolder musicFolder2 = new MusicFolder(2,music2Dir,"Music2",true,new Date());
+ liste.add(musicFolder2);
+ return liste;
+ }
+}
diff --git a/libresonic-main/src/test/java/org/libresonic/player/service/MediaScannerServiceTestCase.java b/libresonic-main/src/test/java/org/libresonic/player/service/MediaScannerServiceTestCase.java
new file mode 100644
index 00000000..1b361d6c
--- /dev/null
+++ b/libresonic-main/src/test/java/org/libresonic/player/service/MediaScannerServiceTestCase.java
@@ -0,0 +1,141 @@
+package org.libresonic.player.service;
+
+import com.codahale.metrics.ConsoleReporter;
+import com.codahale.metrics.JmxReporter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Timer;
+import junit.framework.Assert;
+import junit.framework.TestCase;
+import org.libresonic.player.TestCaseUtils;
+import org.libresonic.player.dao.*;
+import org.libresonic.player.domain.Album;
+import org.libresonic.player.domain.Artist;
+import org.libresonic.player.domain.MediaFile;
+import org.springframework.context.ApplicationContext;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A unit test class to test the MediaScannerService.
+ *
+ * This class uses the Spring application context configuration present in the
+ * /org/libresonic/player/service/mediaScannerServiceTestCase/ directory.
+ *
+ * The media library is found in the /MEDIAS directory.
+ * It is composed of 2 musicFolders (Music and Music2) and several little weight audio files.
+ *
+ * At runtime, the subsonic_home dir is set to target/test-classes/org/libresonic/player/service/mediaScannerServiceTestCase.
+ * An empty database is created on the fly.
+ *
+ */
+public class MediaScannerServiceTestCase extends TestCase {
+
+ private static String baseResources = "/org/libresonic/player/service/mediaScannerServiceTestCase/";
+
+ private final MetricRegistry metrics = new MetricRegistry();
+
+
+ private MediaScannerService mediaScannerService = null;
+ private MediaFileDao mediaFileDao = null;
+ private MusicFolderDao musicFolderDao = null;
+ private DaoHelper daoHelper = null;
+ private MediaFileService mediaFileService = null;
+ private ArtistDao artistDao = null;
+ private AlbumDao albumDao = null;
+
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ TestCaseUtils.setLibresonicHome(baseResources);
+
+ // load spring context
+ ApplicationContext context = TestCaseUtils.loadSpringApplicationContext(baseResources);
+
+ mediaScannerService = (MediaScannerService)context.getBean("mediaScannerService");
+ mediaFileDao = (MediaFileDao)context.getBean("mediaFileDao");
+ musicFolderDao = (MusicFolderDao) context.getBean("musicFolderDao");
+ daoHelper = (DaoHelper) context.getBean("daoHelper");
+ mediaFileService = (MediaFileService) context.getBean("mediaFileService");
+ artistDao = (ArtistDao) context.getBean("artistDao");
+ albumDao = (AlbumDao) context.getBean("albumDao");
+ }
+
+
+ /**
+ * Tests the MediaScannerService by scanning the test media library into an empty database.
+ */
+ public void testScanLibrary() {
+
+ //startMetricsReport();
+
+
+ Timer globalTimer = metrics.timer(MetricRegistry.name(MediaScannerServiceTestCase.class, "Timer.global"));
+
+ Timer.Context globalTimerContext = globalTimer.time();
+ TestCaseUtils.execScan(mediaScannerService);
+ globalTimerContext.stop();
+
+ System.out.println("--- Report of records count per table ---");
+ Map records = TestCaseUtils.recordsInAllTables(daoHelper);
+ records.keySet().forEach(tableName -> System.out.println(tableName+" : "+records.get(tableName).toString() ));
+ System.out.println("--- *********************** ---");
+
+
+ // Music Folder Music must have 3 children
+ List listeMusicChildren = mediaFileDao.getChildrenOf(MusicFolderDaoMock.resolveMusicFolderPath());
+ Assert.assertEquals(3,listeMusicChildren.size());
+ // Music Folder Music2 must have 1 children
+ List listeMusic2Children = mediaFileDao.getChildrenOf(MusicFolderDaoMock.resolveMusic2FolderPath());
+ Assert.assertEquals(1,listeMusic2Children.size());
+
+ System.out.println("--- List of all artists ---");
+ System.out.println("artistName#albumCount");
+ List allArtists = artistDao.getAlphabetialArtists(0,0,musicFolderDao.getAllMusicFolders());
+ allArtists.forEach(artist -> System.out.println(artist.getName()+"#"+artist.getAlbumCount()));
+ System.out.println("--- *********************** ---");
+
+ System.out.println("--- List of all albums ---");
+ System.out.println("name#artist");
+ List allAlbums = albumDao.getAlphabetialAlbums(0,0,true,musicFolderDao.getAllMusicFolders());
+ allAlbums.forEach(album -> System.out.println(album.getName()+"#"+album.getArtist()));
+ Assert.assertEquals(5,allAlbums.size());
+ System.out.println("--- *********************** ---");
+
+
+
+ List listeSongs = mediaFileDao.getSongsByGenre("Baroque Instrumental",0,0,musicFolderDao.getAllMusicFolders());
+
+ // display out metrics report
+ ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics)
+ .convertRatesTo(TimeUnit.SECONDS.SECONDS)
+ .convertDurationsTo(TimeUnit.MILLISECONDS)
+ .build();
+ reporter.report();
+
+ System.out.print("End");
+ }
+
+ /**
+ * Starts a Metrics Console and JMX reporter.
+ */
+ private void startMetricsReport() {
+
+ // Reporter console
+ ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics)
+ .convertRatesTo(TimeUnit.SECONDS.SECONDS)
+ .convertDurationsTo(TimeUnit.MILLISECONDS)
+ .build();
+ reporter.start(10, TimeUnit.SECONDS);
+
+ // Jmx reporter
+ final JmxReporter jmxReporter = JmxReporter.forRegistry(metrics).build();
+ jmxReporter.start();
+ }
+
+
+
+}
diff --git a/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Céline Frisch- Café Zimmermann - Bach- Goldberg Variations, Canons [Disc 1]/01 - Bach- Goldberg Variations, BWV 988 - Aria.flac b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Céline Frisch- Café Zimmermann - Bach- Goldberg Variations, Canons [Disc 1]/01 - Bach- Goldberg Variations, BWV 988 - Aria.flac
new file mode 100755
index 00000000..f0bdbc69
Binary files /dev/null and b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Céline Frisch- Café Zimmermann - Bach- Goldberg Variations, Canons [Disc 1]/01 - Bach- Goldberg Variations, BWV 988 - Aria.flac differ
diff --git a/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Céline Frisch- Café Zimmermann - Bach- Goldberg Variations, Canons [Disc 1]/02 - Bach- Goldberg Variations, BWV 988 - Variatio 1 A 1 Clav..flac b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Céline Frisch- Café Zimmermann - Bach- Goldberg Variations, Canons [Disc 1]/02 - Bach- Goldberg Variations, BWV 988 - Variatio 1 A 1 Clav..flac
new file mode 100755
index 00000000..09ca1c75
Binary files /dev/null and b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Céline Frisch- Café Zimmermann - Bach- Goldberg Variations, Canons [Disc 1]/02 - Bach- Goldberg Variations, BWV 988 - Variatio 1 A 1 Clav..flac differ
diff --git a/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Céline Frisch- Café Zimmermann - Bach- Goldberg Variations, Canons [Disc 1]/Folder.jpg b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Céline Frisch- Café Zimmermann - Bach- Goldberg Variations, Canons [Disc 1]/Folder.jpg
new file mode 100644
index 00000000..f1222e13
Binary files /dev/null and b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Céline Frisch- Café Zimmermann - Bach- Goldberg Variations, Canons [Disc 1]/Folder.jpg differ
diff --git a/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Ravel/_DIR_ Ravel - Chamber Music With Voice/01 - Sonata Violin & Cello I. Allegro.ogg b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Ravel/_DIR_ Ravel - Chamber Music With Voice/01 - Sonata Violin & Cello I. Allegro.ogg
new file mode 100755
index 00000000..62199cfa
Binary files /dev/null and b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Ravel/_DIR_ Ravel - Chamber Music With Voice/01 - Sonata Violin & Cello I. Allegro.ogg differ
diff --git a/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Ravel/_DIR_ Ravel - Chamber Music With Voice/02 - Sonata Violin & Cello II. Tres Vif.ogg b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Ravel/_DIR_ Ravel - Chamber Music With Voice/02 - Sonata Violin & Cello II. Tres Vif.ogg
new file mode 100755
index 00000000..7f8f91cd
Binary files /dev/null and b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Ravel/_DIR_ Ravel - Chamber Music With Voice/02 - Sonata Violin & Cello II. Tres Vif.ogg differ
diff --git a/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Ravel/_DIR_ Ravel - Chamber Music With Voice/Folder.jpg b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Ravel/_DIR_ Ravel - Chamber Music With Voice/Folder.jpg
new file mode 100644
index 00000000..b902207b
Binary files /dev/null and b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Ravel/_DIR_ Ravel - Chamber Music With Voice/Folder.jpg differ
diff --git a/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Ravel/_DIR_ Ravel - Complete Piano Works/01 - Gaspard de la Nuit - i. Ondine.mp3 b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Ravel/_DIR_ Ravel - Complete Piano Works/01 - Gaspard de la Nuit - i. Ondine.mp3
new file mode 100644
index 00000000..fcbea827
Binary files /dev/null and b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Ravel/_DIR_ Ravel - Complete Piano Works/01 - Gaspard de la Nuit - i. Ondine.mp3 differ
diff --git a/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Ravel/_DIR_ Ravel - Complete Piano Works/02 - Gaspard de la Nuit - ii. Le Gibet.mp3 b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Ravel/_DIR_ Ravel - Complete Piano Works/02 - Gaspard de la Nuit - ii. Le Gibet.mp3
new file mode 100644
index 00000000..85ce05ef
Binary files /dev/null and b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Ravel/_DIR_ Ravel - Complete Piano Works/02 - Gaspard de la Nuit - ii. Le Gibet.mp3 differ
diff --git a/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Ravel/_DIR_ Ravel - Complete Piano Works/Folder.jpeg b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Ravel/_DIR_ Ravel - Complete Piano Works/Folder.jpeg
new file mode 100644
index 00000000..6542198c
Binary files /dev/null and b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Ravel/_DIR_ Ravel - Complete Piano Works/Folder.jpeg differ
diff --git a/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Sixteen Horsepower/_DIR_ Sackcloth 'n' Ashes/Sixteen Horsepower - 01 I Seen What I Saw.mp3 b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Sixteen Horsepower/_DIR_ Sackcloth 'n' Ashes/Sixteen Horsepower - 01 I Seen What I Saw.mp3
new file mode 100755
index 00000000..31aec22b
Binary files /dev/null and b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Sixteen Horsepower/_DIR_ Sackcloth 'n' Ashes/Sixteen Horsepower - 01 I Seen What I Saw.mp3 differ
diff --git a/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Sixteen Horsepower/_DIR_ Sackcloth 'n' Ashes/Sixteen Horsepower - 02 Black Soul Choir.mp3 b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Sixteen Horsepower/_DIR_ Sackcloth 'n' Ashes/Sixteen Horsepower - 02 Black Soul Choir.mp3
new file mode 100755
index 00000000..1eae1f7c
Binary files /dev/null and b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Sixteen Horsepower/_DIR_ Sackcloth 'n' Ashes/Sixteen Horsepower - 02 Black Soul Choir.mp3 differ
diff --git a/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Sixteen Horsepower/_DIR_ Sackcloth 'n' Ashes/Sixteen Horsepower - 10 Reed Neck Reel.mp3 b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Sixteen Horsepower/_DIR_ Sackcloth 'n' Ashes/Sixteen Horsepower - 10 Reed Neck Reel.mp3
new file mode 100755
index 00000000..5f47ebad
Binary files /dev/null and b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Sixteen Horsepower/_DIR_ Sackcloth 'n' Ashes/Sixteen Horsepower - 10 Reed Neck Reel.mp3 differ
diff --git a/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Sixteen Horsepower/_DIR_ Sackcloth 'n' Ashes/cover.jpg b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Sixteen Horsepower/_DIR_ Sackcloth 'n' Ashes/cover.jpg
new file mode 100644
index 00000000..0c2e6181
Binary files /dev/null and b/libresonic-main/src/test/resources/MEDIAS/Music/_DIR_ Sixteen Horsepower/_DIR_ Sackcloth 'n' Ashes/cover.jpg differ
diff --git a/libresonic-main/src/test/resources/MEDIAS/Music2/_DIR_ chrome hoof - 2004/02 eyes like dull hazlenuts.mp3 b/libresonic-main/src/test/resources/MEDIAS/Music2/_DIR_ chrome hoof - 2004/02 eyes like dull hazlenuts.mp3
new file mode 100755
index 00000000..d9d3fc99
Binary files /dev/null and b/libresonic-main/src/test/resources/MEDIAS/Music2/_DIR_ chrome hoof - 2004/02 eyes like dull hazlenuts.mp3 differ
diff --git a/libresonic-main/src/test/resources/MEDIAS/Music2/_DIR_ chrome hoof - 2004/10 telegraph hill.mp3 b/libresonic-main/src/test/resources/MEDIAS/Music2/_DIR_ chrome hoof - 2004/10 telegraph hill.mp3
new file mode 100755
index 00000000..1acc2726
Binary files /dev/null and b/libresonic-main/src/test/resources/MEDIAS/Music2/_DIR_ chrome hoof - 2004/10 telegraph hill.mp3 differ
diff --git a/libresonic-main/src/test/resources/MEDIAS/Music2/_DIR_ chrome hoof - 2004/Folder.jpg b/libresonic-main/src/test/resources/MEDIAS/Music2/_DIR_ chrome hoof - 2004/Folder.jpg
new file mode 100644
index 00000000..3bdbd8b3
Binary files /dev/null and b/libresonic-main/src/test/resources/MEDIAS/Music2/_DIR_ chrome hoof - 2004/Folder.jpg differ
diff --git a/libresonic-main/src/test/resources/org/libresonic/player/service/mediaScannerServiceTestCase/applicationContext-cache.xml b/libresonic-main/src/test/resources/org/libresonic/player/service/mediaScannerServiceTestCase/applicationContext-cache.xml
new file mode 100644
index 00000000..da4c296a
--- /dev/null
+++ b/libresonic-main/src/test/resources/org/libresonic/player/service/mediaScannerServiceTestCase/applicationContext-cache.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libresonic-main/src/test/resources/org/libresonic/player/service/mediaScannerServiceTestCase/applicationContext-service.xml b/libresonic-main/src/test/resources/org/libresonic/player/service/mediaScannerServiceTestCase/applicationContext-service.xml
new file mode 100644
index 00000000..2d0b320f
--- /dev/null
+++ b/libresonic-main/src/test/resources/org/libresonic/player/service/mediaScannerServiceTestCase/applicationContext-service.xml
@@ -0,0 +1,186 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+