parent
							
								
									42bced139f
								
							
						
					
					
						commit
						767b39ed5b
					
				| @ -0,0 +1,121 @@ | |||||||
|  | /* | ||||||
|  |  This file is part of Airsonic. | ||||||
|  | 
 | ||||||
|  |  Airsonic is free software: you can redistribute it and/or modify | ||||||
|  |  it under the terms of the GNU General Public License as published by | ||||||
|  |  the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |  Airsonic 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 General Public License for more details. | ||||||
|  | 
 | ||||||
|  |  You should have received a copy of the GNU General Public License | ||||||
|  |  along with Airsonic.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | 
 | ||||||
|  |  Copyright 2016 (C) Airsonic Authors | ||||||
|  |  Based upon Subsonic, Copyright 2009 (C) Sindre Mehus | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package org.airsonic.player.service.search; | ||||||
|  | 
 | ||||||
|  | import org.apache.lucene.analysis.ASCIIFoldingFilter; | ||||||
|  | import org.apache.lucene.analysis.Analyzer; | ||||||
|  | import org.apache.lucene.analysis.LowerCaseFilter; | ||||||
|  | import org.apache.lucene.analysis.StopFilter; | ||||||
|  | import org.apache.lucene.analysis.TokenStream; | ||||||
|  | import org.apache.lucene.analysis.standard.StandardAnalyzer; | ||||||
|  | import org.apache.lucene.analysis.standard.StandardFilter; | ||||||
|  | import org.apache.lucene.analysis.standard.StandardTokenizer; | ||||||
|  | import org.apache.lucene.util.Version; | ||||||
|  | import org.springframework.stereotype.Component; | ||||||
|  | 
 | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.io.Reader; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Analyzer provider. | ||||||
|  |  * This class is a division of what was once part of SearchService and added functionality. | ||||||
|  |  * This class provides Analyzer which is used at index generation | ||||||
|  |  * and QueryAnalyzer which analyzes the specified query at search time. | ||||||
|  |  * Analyzer can be closed but is a reuse premise. | ||||||
|  |  * It is held in this class. | ||||||
|  |  */ | ||||||
|  | @Component | ||||||
|  | public final class AnalyzerFactory { | ||||||
|  | 
 | ||||||
|  |     private Analyzer analyzer; | ||||||
|  | 
 | ||||||
|  |     private Analyzer queryAnalyzer; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Return analyzer. | ||||||
|  |      *  | ||||||
|  |      * @return analyzer for index | ||||||
|  |      */ | ||||||
|  |     public Analyzer getAnalyzer() { | ||||||
|  |         if (null == analyzer) { | ||||||
|  |             analyzer = new CustomAnalyzer(); | ||||||
|  |         } | ||||||
|  |         return analyzer; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Return analyzer. | ||||||
|  |      *  | ||||||
|  |      * @return analyzer for index | ||||||
|  |      */ | ||||||
|  |     public Analyzer getQueryAnalyzer() { | ||||||
|  |         if (null == queryAnalyzer) { | ||||||
|  |             queryAnalyzer = new CustomAnalyzer(); | ||||||
|  |         } | ||||||
|  |         return queryAnalyzer; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /* | ||||||
|  |      * The legacy CustomAnalyzer implementation is kept as it is. | ||||||
|  |      */ | ||||||
|  |     private class CustomAnalyzer extends StandardAnalyzer { | ||||||
|  |         private CustomAnalyzer() { | ||||||
|  |             /* | ||||||
|  |              * Version.LUCENE_30 | ||||||
|  |              * It is a transient description and will be deleted when upgrading the version. | ||||||
|  |              * SearchService variables are not used because the reference direction conflicts. | ||||||
|  |              */ | ||||||
|  |             super(Version.LUCENE_30); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public TokenStream tokenStream(String fieldName, Reader reader) { | ||||||
|  |             TokenStream result = super.tokenStream(fieldName, reader); | ||||||
|  |             return new ASCIIFoldingFilter(result); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public TokenStream reusableTokenStream(String fieldName, Reader reader) throws IOException { | ||||||
|  |             class SavedStreams { | ||||||
|  |                 StandardTokenizer tokenStream; | ||||||
|  |                 TokenStream       filteredTokenStream; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             SavedStreams streams = (SavedStreams) getPreviousTokenStream(); | ||||||
|  |             if (streams == null) { | ||||||
|  |                 streams = new SavedStreams(); | ||||||
|  |                 setPreviousTokenStream(streams); | ||||||
|  |                 streams.tokenStream = new StandardTokenizer(Version.LUCENE_30, reader); | ||||||
|  |                 streams.filteredTokenStream = new StandardFilter(streams.tokenStream); | ||||||
|  |                 streams.filteredTokenStream = new LowerCaseFilter(streams.filteredTokenStream); | ||||||
|  |                 streams.filteredTokenStream = new StopFilter(true, streams.filteredTokenStream, | ||||||
|  |                         STOP_WORDS_SET); | ||||||
|  |                 streams.filteredTokenStream = new ASCIIFoldingFilter(streams.filteredTokenStream); | ||||||
|  |             } else { | ||||||
|  |                 streams.tokenStream.reset(reader); | ||||||
|  |             } | ||||||
|  |             streams.tokenStream.setMaxTokenLength(DEFAULT_MAX_TOKEN_LENGTH); | ||||||
|  | 
 | ||||||
|  |             return streams.filteredTokenStream; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,181 @@ | |||||||
|  | /* | ||||||
|  |  This file is part of Airsonic. | ||||||
|  | 
 | ||||||
|  |  Airsonic is free software: you can redistribute it and/or modify | ||||||
|  |  it under the terms of the GNU General Public License as published by | ||||||
|  |  the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |  Airsonic 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 General Public License for more details. | ||||||
|  | 
 | ||||||
|  |  You should have received a copy of the GNU General Public License | ||||||
|  |  along with Airsonic.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | 
 | ||||||
|  |  Copyright 2016 (C) Airsonic Authors | ||||||
|  |  Based upon Subsonic, Copyright 2009 (C) Sindre Mehus | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package org.airsonic.player.service.search; | ||||||
|  | 
 | ||||||
|  | import org.airsonic.player.domain.Album; | ||||||
|  | import org.airsonic.player.domain.Artist; | ||||||
|  | import org.airsonic.player.domain.MediaFile; | ||||||
|  | import org.airsonic.player.domain.MusicFolder; | ||||||
|  | import org.apache.lucene.document.Document; | ||||||
|  | import org.apache.lucene.document.Field; | ||||||
|  | import org.apache.lucene.document.NumericField; | ||||||
|  | import org.springframework.stereotype.Component; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A factory that generates the documents to be stored in the index. | ||||||
|  |  */ | ||||||
|  | @Component | ||||||
|  | public class DocumentFactory { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Normalize the genre string. | ||||||
|  |      *  | ||||||
|  |      * @param genre genre string | ||||||
|  |      * @return genre string normalized | ||||||
|  |      * @deprecated should be resolved with tokenizer or filter | ||||||
|  |      */ | ||||||
|  |     @Deprecated | ||||||
|  |     private String normalizeGenre(String genre) { | ||||||
|  |         return genre.toLowerCase().replace(" ", "").replace("-", ""); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Create a document. | ||||||
|  |      *  | ||||||
|  |      * @param mediaFile target of document | ||||||
|  |      * @return document | ||||||
|  |      * @since legacy | ||||||
|  |      */ | ||||||
|  |     public Document createAlbumDocument(MediaFile mediaFile) { | ||||||
|  |         Document doc = new Document(); | ||||||
|  |         doc.add(new NumericField(FieldNames.ID, Field.Store.YES, false) | ||||||
|  |                 .setIntValue(mediaFile.getId())); | ||||||
|  | 
 | ||||||
|  |         if (mediaFile.getArtist() != null) { | ||||||
|  |             doc.add(new Field(FieldNames.ARTIST, mediaFile.getArtist(), Field.Store.YES, | ||||||
|  |                     Field.Index.ANALYZED)); | ||||||
|  |         } | ||||||
|  |         if (mediaFile.getAlbumName() != null) { | ||||||
|  |             doc.add(new Field(FieldNames.ALBUM, mediaFile.getAlbumName(), Field.Store.YES, | ||||||
|  |                     Field.Index.ANALYZED)); | ||||||
|  |         } | ||||||
|  |         if (mediaFile.getFolder() != null) { | ||||||
|  |             doc.add(new Field(FieldNames.FOLDER, mediaFile.getFolder(), Field.Store.NO, | ||||||
|  |                     Field.Index.NOT_ANALYZED_NO_NORMS)); | ||||||
|  |         } | ||||||
|  |         return doc; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Create a document. | ||||||
|  |      *  | ||||||
|  |      * @param mediaFile target of document | ||||||
|  |      * @return document | ||||||
|  |      * @since legacy | ||||||
|  |      */ | ||||||
|  |     public Document createArtistDocument(MediaFile mediaFile) { | ||||||
|  |         Document doc = new Document(); | ||||||
|  |         doc.add(new NumericField(FieldNames.ID, Field.Store.YES, false) | ||||||
|  |                 .setIntValue(mediaFile.getId())); | ||||||
|  | 
 | ||||||
|  |         if (mediaFile.getArtist() != null) { | ||||||
|  |             doc.add(new Field(FieldNames.ARTIST, mediaFile.getArtist(), Field.Store.YES, | ||||||
|  |                     Field.Index.ANALYZED)); | ||||||
|  |         } | ||||||
|  |         if (mediaFile.getFolder() != null) { | ||||||
|  |             doc.add(new Field(FieldNames.FOLDER, mediaFile.getFolder(), Field.Store.NO, | ||||||
|  |                     Field.Index.NOT_ANALYZED_NO_NORMS)); | ||||||
|  |         } | ||||||
|  |         return doc; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Create a document. | ||||||
|  |      *  | ||||||
|  |      * @param album target of document | ||||||
|  |      * @return document | ||||||
|  |      * @since legacy | ||||||
|  |      */ | ||||||
|  |     public Document createAlbumId3Document(Album album) { | ||||||
|  |         Document doc = new Document(); | ||||||
|  |         doc.add(new NumericField(FieldNames.ID, Field.Store.YES, false).setIntValue(album.getId())); | ||||||
|  | 
 | ||||||
|  |         if (album.getArtist() != null) { | ||||||
|  |             doc.add(new Field(FieldNames.ARTIST, album.getArtist(), Field.Store.YES, | ||||||
|  |                     Field.Index.ANALYZED)); | ||||||
|  |         } | ||||||
|  |         if (album.getName() != null) { | ||||||
|  |             doc.add(new Field(FieldNames.ALBUM, album.getName(), Field.Store.YES, | ||||||
|  |                     Field.Index.ANALYZED)); | ||||||
|  |         } | ||||||
|  |         if (album.getFolderId() != null) { | ||||||
|  |             doc.add(new NumericField(FieldNames.FOLDER_ID, Field.Store.NO, true) | ||||||
|  |                     .setIntValue(album.getFolderId())); | ||||||
|  |         } | ||||||
|  |         return doc; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Create a document. | ||||||
|  |      *  | ||||||
|  |      * @param artist target of document | ||||||
|  |      * @param musicFolder target folder exists | ||||||
|  |      * @return document | ||||||
|  |      * @since legacy | ||||||
|  |      */ | ||||||
|  |     public Document createArtistId3Document(Artist artist, MusicFolder musicFolder) { | ||||||
|  |         Document doc = new Document(); | ||||||
|  |         doc.add(new NumericField(FieldNames.ID, Field.Store.YES, false) | ||||||
|  |                 .setIntValue(artist.getId())); | ||||||
|  |         doc.add(new Field(FieldNames.ARTIST, artist.getName(), Field.Store.YES, | ||||||
|  |                 Field.Index.ANALYZED)); | ||||||
|  |         doc.add(new NumericField(FieldNames.FOLDER_ID, Field.Store.NO, true) | ||||||
|  |                 .setIntValue(musicFolder.getId())); | ||||||
|  |         return doc; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Create a document. | ||||||
|  |      *  | ||||||
|  |      * @param mediaFile target of document | ||||||
|  |      * @return document | ||||||
|  |      * @since legacy | ||||||
|  |      */ | ||||||
|  |     public Document createSongDocument(MediaFile mediaFile) { | ||||||
|  |         Document doc = new Document(); | ||||||
|  |         doc.add(new NumericField(FieldNames.ID, Field.Store.YES, false) | ||||||
|  |                 .setIntValue(mediaFile.getId())); | ||||||
|  |         doc.add(new Field(FieldNames.MEDIA_TYPE, mediaFile.getMediaType().name(), Field.Store.NO, | ||||||
|  |                 Field.Index.ANALYZED_NO_NORMS)); | ||||||
|  |         if (mediaFile.getTitle() != null) { | ||||||
|  |             doc.add(new Field(FieldNames.TITLE, mediaFile.getTitle(), Field.Store.YES, | ||||||
|  |                     Field.Index.ANALYZED)); | ||||||
|  |         } | ||||||
|  |         if (mediaFile.getArtist() != null) { | ||||||
|  |             doc.add(new Field(FieldNames.ARTIST, mediaFile.getArtist(), Field.Store.YES, | ||||||
|  |                     Field.Index.ANALYZED)); | ||||||
|  |         } | ||||||
|  |         if (mediaFile.getGenre() != null) { | ||||||
|  |             doc.add(new Field(FieldNames.GENRE, normalizeGenre(mediaFile.getGenre()), | ||||||
|  |                     Field.Store.NO, Field.Index.ANALYZED)); | ||||||
|  |         } | ||||||
|  |         if (mediaFile.getYear() != null) { | ||||||
|  |             doc.add(new NumericField(FieldNames.YEAR, Field.Store.NO, true) | ||||||
|  |                     .setIntValue(mediaFile.getYear())); | ||||||
|  |         } | ||||||
|  |         if (mediaFile.getFolder() != null) { | ||||||
|  |             doc.add(new Field(FieldNames.FOLDER, mediaFile.getFolder(), Field.Store.NO, | ||||||
|  |                     Field.Index.NOT_ANALYZED_NO_NORMS)); | ||||||
|  |         } | ||||||
|  |         return doc; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,95 @@ | |||||||
|  | /* | ||||||
|  |  This file is part of Airsonic. | ||||||
|  | 
 | ||||||
|  |  Airsonic is free software: you can redistribute it and/or modify | ||||||
|  |  it under the terms of the GNU General Public License as published by | ||||||
|  |  the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |  Airsonic 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 General Public License for more details. | ||||||
|  | 
 | ||||||
|  |  You should have received a copy of the GNU General Public License | ||||||
|  |  along with Airsonic.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | 
 | ||||||
|  |  Copyright 2016 (C) Airsonic Authors | ||||||
|  |  Based upon Subsonic, Copyright 2009 (C) Sindre Mehus | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package org.airsonic.player.service.search; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Enum that symbolizes the field name used for lucene index. | ||||||
|  |  * This class is a division of what was once part of SearchService and added functionality. | ||||||
|  |  */ | ||||||
|  | class FieldNames { | ||||||
|  | 
 | ||||||
|  |     private FieldNames() { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * A field same to a legacy server, id field. | ||||||
|  |      *  | ||||||
|  |      * @since legacy | ||||||
|  |      **/ | ||||||
|  |     public static final String ID = "id"; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * A field same to a legacy server, id field. | ||||||
|  |      *  | ||||||
|  |      * @since legacy | ||||||
|  |      **/ | ||||||
|  |     public static final String FOLDER_ID = "folderId"; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * A field same to a legacy server, numeric field. | ||||||
|  |      *  | ||||||
|  |      * @since legacy | ||||||
|  |      **/ | ||||||
|  |     public static final String YEAR = "year"; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * A field same to a legacy server, key field. | ||||||
|  |      *  | ||||||
|  |      * @since legacy | ||||||
|  |      **/ | ||||||
|  |     public static final String GENRE = "genre"; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * A field same to a legacy server, key field. | ||||||
|  |      *  | ||||||
|  |      * @since legacy | ||||||
|  |      **/ | ||||||
|  |     public static final String MEDIA_TYPE = "mediaType"; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * A field same to a legacy server, key field. | ||||||
|  |      *  | ||||||
|  |      * @since legacy | ||||||
|  |      **/ | ||||||
|  |     public static final String FOLDER = "folder"; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * A field same to a legacy server, usually with common word parsing. | ||||||
|  |      *  | ||||||
|  |      * @since legacy | ||||||
|  |      **/ | ||||||
|  |     public static final String ARTIST = "artist"; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * A field same to a legacy server, usually with common word parsing. | ||||||
|  |      *  | ||||||
|  |      * @since legacy | ||||||
|  |      **/ | ||||||
|  |     public static final String ALBUM = "album"; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * A field same to a legacy server, usually with common word parsing. | ||||||
|  |      *  | ||||||
|  |      * @since legacy | ||||||
|  |      **/ | ||||||
|  |     public static final String TITLE = "title"; | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,169 @@ | |||||||
|  | /* | ||||||
|  |  This file is part of Airsonic. | ||||||
|  | 
 | ||||||
|  |  Airsonic is free software: you can redistribute it and/or modify | ||||||
|  |  it under the terms of the GNU General Public License as published by | ||||||
|  |  the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |  Airsonic 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 General Public License for more details. | ||||||
|  | 
 | ||||||
|  |  You should have received a copy of the GNU General Public License | ||||||
|  |  along with Airsonic.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | 
 | ||||||
|  |  Copyright 2016 (C) Airsonic Authors | ||||||
|  |  Based upon Subsonic, Copyright 2009 (C) Sindre Mehus | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package org.airsonic.player.service.search; | ||||||
|  | 
 | ||||||
|  | import org.airsonic.player.domain.Album; | ||||||
|  | import org.airsonic.player.domain.Artist; | ||||||
|  | import org.airsonic.player.domain.MediaFile; | ||||||
|  | import org.airsonic.player.domain.MusicFolder; | ||||||
|  | import org.airsonic.player.service.SettingsService; | ||||||
|  | import org.airsonic.player.util.FileUtil; | ||||||
|  | import org.apache.lucene.index.IndexReader; | ||||||
|  | import org.apache.lucene.index.IndexWriter; | ||||||
|  | import org.apache.lucene.store.FSDirectory; | ||||||
|  | import org.slf4j.Logger; | ||||||
|  | import org.slf4j.LoggerFactory; | ||||||
|  | import org.springframework.beans.factory.annotation.Autowired; | ||||||
|  | import org.springframework.stereotype.Component; | ||||||
|  | 
 | ||||||
|  | import java.io.File; | ||||||
|  | import java.io.IOException; | ||||||
|  | 
 | ||||||
|  | import static org.airsonic.player.service.search.IndexType.ALBUM; | ||||||
|  | import static org.airsonic.player.service.search.IndexType.ALBUM_ID3; | ||||||
|  | import static org.airsonic.player.service.search.IndexType.ARTIST; | ||||||
|  | import static org.airsonic.player.service.search.IndexType.ARTIST_ID3; | ||||||
|  | import static org.airsonic.player.service.search.IndexType.SONG; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Function class that is strongly linked to the lucene index implementation. | ||||||
|  |  * Legacy has an implementation in SearchService. | ||||||
|  |  *  | ||||||
|  |  * If the index CRUD and search functionality are in the same class, | ||||||
|  |  * there is often a dependency conflict on the class used. | ||||||
|  |  * Although the interface of SearchService is left to maintain the legacy implementation, | ||||||
|  |  * it is desirable that methods of index operations other than search essentially use this class directly. | ||||||
|  |  */ | ||||||
|  | @Component | ||||||
|  | public class IndexManager { | ||||||
|  | 
 | ||||||
|  |     private static final Logger LOG = LoggerFactory.getLogger(IndexManager.class); | ||||||
|  | 
 | ||||||
|  |     @Autowired | ||||||
|  |     private AnalyzerFactory analyzerFactory; | ||||||
|  | 
 | ||||||
|  |     @Autowired | ||||||
|  |     private DocumentFactory documentFactory; | ||||||
|  | 
 | ||||||
|  |     private IndexWriter artistWriter; | ||||||
|  |     private IndexWriter artistId3Writer; | ||||||
|  |     private IndexWriter albumWriter; | ||||||
|  |     private IndexWriter albumId3Writer; | ||||||
|  |     private IndexWriter songWriter; | ||||||
|  | 
 | ||||||
|  |     public void index(Album album) { | ||||||
|  |         try { | ||||||
|  |             albumId3Writer.addDocument(documentFactory.createAlbumId3Document(album)); | ||||||
|  |         } catch (Exception x) { | ||||||
|  |             LOG.error("Failed to create search index for " + album, x); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void index(Artist artist, MusicFolder musicFolder) { | ||||||
|  |         try { | ||||||
|  |             artistId3Writer | ||||||
|  |                     .addDocument(documentFactory.createArtistId3Document(artist, musicFolder)); | ||||||
|  |         } catch (Exception x) { | ||||||
|  |             LOG.error("Failed to create search index for " + artist, x); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void index(MediaFile mediaFile) { | ||||||
|  |         try { | ||||||
|  |             if (mediaFile.isFile()) { | ||||||
|  |                 songWriter.addDocument(documentFactory.createSongDocument(mediaFile)); | ||||||
|  |             } else if (mediaFile.isAlbum()) { | ||||||
|  |                 albumWriter.addDocument(documentFactory.createAlbumDocument(mediaFile)); | ||||||
|  |             } else { | ||||||
|  |                 artistWriter.addDocument(documentFactory.createArtistDocument(mediaFile)); | ||||||
|  |             } | ||||||
|  |         } catch (Exception x) { | ||||||
|  |             LOG.error("Failed to create search index for " + mediaFile, x); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static final String LUCENE_DIR = "lucene2"; | ||||||
|  | 
 | ||||||
|  |     public IndexReader createIndexReader(IndexType indexType) throws IOException { | ||||||
|  |         File dir = getIndexDirectory(indexType); | ||||||
|  |         return IndexReader.open(FSDirectory.open(dir), true); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * It is static as an intermediate response of the transition period. | ||||||
|  |      * (It is called before injection because it is called by SearchService constructor) | ||||||
|  |      *  | ||||||
|  |      * @return | ||||||
|  |      */ | ||||||
|  |     private static File getIndexRootDirectory() { | ||||||
|  |         return new File(SettingsService.getAirsonicHome(), LUCENE_DIR); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Make it public as an interim response of the transition period. | ||||||
|  |      * (It is called before the injection because it is called in the SearchService constructor.) | ||||||
|  |      *  | ||||||
|  |      * @param indexType | ||||||
|  |      * @return | ||||||
|  |      * @deprecated It should not be called from outside. | ||||||
|  |      */ | ||||||
|  |     @Deprecated | ||||||
|  |     public static File getIndexDirectory(IndexType indexType) { | ||||||
|  |         return new File(getIndexRootDirectory(), indexType.toString().toLowerCase()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private IndexWriter createIndexWriter(IndexType indexType) throws IOException { | ||||||
|  |         File dir = getIndexDirectory(indexType); | ||||||
|  |         return new IndexWriter(FSDirectory.open(dir), analyzerFactory.getAnalyzer(), true, | ||||||
|  |                 new IndexWriter.MaxFieldLength(10)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public final void startIndexing() { | ||||||
|  |         try { | ||||||
|  |             artistWriter = createIndexWriter(ARTIST); | ||||||
|  |             artistId3Writer = createIndexWriter(ARTIST_ID3); | ||||||
|  |             albumWriter = createIndexWriter(ALBUM); | ||||||
|  |             albumId3Writer = createIndexWriter(ALBUM_ID3); | ||||||
|  |             songWriter = createIndexWriter(SONG); | ||||||
|  |         } catch (Exception x) { | ||||||
|  |             LOG.error("Failed to create search index.", x); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void stopIndexing() { | ||||||
|  |         try { | ||||||
|  |             artistWriter.optimize(); | ||||||
|  |             artistId3Writer.optimize(); | ||||||
|  |             albumWriter.optimize(); | ||||||
|  |             albumId3Writer.optimize(); | ||||||
|  |             songWriter.optimize(); | ||||||
|  |         } catch (Exception x) { | ||||||
|  |             LOG.error("Failed to create search index.", x); | ||||||
|  |         } finally { | ||||||
|  |             FileUtil.closeQuietly(artistId3Writer); | ||||||
|  |             FileUtil.closeQuietly(artistWriter); | ||||||
|  |             FileUtil.closeQuietly(albumWriter); | ||||||
|  |             FileUtil.closeQuietly(albumId3Writer); | ||||||
|  |             FileUtil.closeQuietly(songWriter); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,231 @@ | |||||||
|  | /* | ||||||
|  |  This file is part of Airsonic. | ||||||
|  | 
 | ||||||
|  |  Airsonic is free software: you can redistribute it and/or modify | ||||||
|  |  it under the terms of the GNU General Public License as published by | ||||||
|  |  the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |  Airsonic 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 General Public License for more details. | ||||||
|  | 
 | ||||||
|  |  You should have received a copy of the GNU General Public License | ||||||
|  |  along with Airsonic.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | 
 | ||||||
|  |  Copyright 2016 (C) Airsonic Authors | ||||||
|  |  Based upon Subsonic, Copyright 2009 (C) Sindre Mehus | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package org.airsonic.player.service.search; | ||||||
|  | 
 | ||||||
|  | import org.airsonic.player.domain.MediaFile; | ||||||
|  | import org.airsonic.player.domain.MusicFolder; | ||||||
|  | import org.airsonic.player.domain.RandomSearchCriteria; | ||||||
|  | import org.airsonic.player.domain.SearchCriteria; | ||||||
|  | import org.apache.lucene.analysis.ASCIIFoldingFilter; | ||||||
|  | import org.apache.lucene.analysis.standard.StandardTokenizer; | ||||||
|  | import org.apache.lucene.analysis.tokenattributes.TermAttribute; | ||||||
|  | import org.apache.lucene.index.Term; | ||||||
|  | import org.apache.lucene.queryParser.MultiFieldQueryParser; | ||||||
|  | import org.apache.lucene.queryParser.ParseException; | ||||||
|  | import org.apache.lucene.queryParser.QueryParser; | ||||||
|  | import org.apache.lucene.search.BooleanClause; | ||||||
|  | import org.apache.lucene.search.BooleanQuery; | ||||||
|  | import org.apache.lucene.search.NumericRangeQuery; | ||||||
|  | import org.apache.lucene.search.Query; | ||||||
|  | import org.apache.lucene.search.TermQuery; | ||||||
|  | import org.apache.lucene.search.spans.SpanOrQuery; | ||||||
|  | import org.apache.lucene.search.spans.SpanQuery; | ||||||
|  | import org.apache.lucene.search.spans.SpanTermQuery; | ||||||
|  | import org.apache.lucene.util.NumericUtils; | ||||||
|  | import org.apache.lucene.util.Version; | ||||||
|  | import org.springframework.beans.factory.annotation.Autowired; | ||||||
|  | import org.springframework.stereotype.Component; | ||||||
|  | 
 | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.io.StringReader; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | import static org.airsonic.player.service.search.IndexType.ALBUM_ID3; | ||||||
|  | import static org.airsonic.player.service.search.IndexType.ARTIST_ID3; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Factory class of Lucene Query. | ||||||
|  |  * This class is an extract of the functionality that was once part of SearchService. | ||||||
|  |  * It is for maintainability and verification. | ||||||
|  |  * Each corresponds to the SearchService method. | ||||||
|  |  * The API syntax for query generation depends on the lucene version. | ||||||
|  |  * verification with query grammar is possible. | ||||||
|  |  * On the other hand, the generated queries are relatively small by version. | ||||||
|  |  * Therefore, test cases of this class are useful for large version upgrades. | ||||||
|  |  **/ | ||||||
|  | @Component | ||||||
|  | public class QueryFactory { | ||||||
|  | 
 | ||||||
|  |     @Autowired | ||||||
|  |     private AnalyzerFactory analyzerFactory; | ||||||
|  | 
 | ||||||
|  |     private String analyzeQuery(String query) throws IOException { | ||||||
|  |         StringBuilder result = new StringBuilder(); | ||||||
|  |         /* | ||||||
|  |          * Version.LUCENE_30 | ||||||
|  |          * It is a transient description and will be deleted when upgrading the version. | ||||||
|  |          * SearchService variables are not used because the reference direction conflicts. | ||||||
|  |          */ | ||||||
|  |         @SuppressWarnings("resource") | ||||||
|  |         ASCIIFoldingFilter filter = new ASCIIFoldingFilter( | ||||||
|  |                 new StandardTokenizer(Version.LUCENE_30, new StringReader(query))); | ||||||
|  |         TermAttribute termAttribute = filter.getAttribute(TermAttribute.class); | ||||||
|  |         while (filter.incrementToken()) { | ||||||
|  |             result.append(termAttribute.term()).append("* "); | ||||||
|  |         } | ||||||
|  |         return result.toString(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Normalize the genre string. | ||||||
|  |      *  | ||||||
|  |      * @param genre genre string | ||||||
|  |      * @return genre string normalized | ||||||
|  |      * @deprecated should be resolved with tokenizer or filter | ||||||
|  |      */ | ||||||
|  |     @Deprecated | ||||||
|  |     private String normalizeGenre(String genre) { | ||||||
|  |         return genre.toLowerCase().replace(" ", "").replace("-", ""); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Query generation expression extracted from | ||||||
|  |      * {@link org.airsonic.player.service.SearchService#search(SearchCriteria, List, IndexType)}. | ||||||
|  |      *  | ||||||
|  |      * @param criteria criteria | ||||||
|  |      * @param musicFolders musicFolders | ||||||
|  |      * @param indexType {@link IndexType} | ||||||
|  |      * @return Query | ||||||
|  |      * @throws IOException When parsing of MultiFieldQueryParser fails | ||||||
|  |      * @throws ParseException When parsing of MultiFieldQueryParser fails | ||||||
|  |      */ | ||||||
|  |     public Query search(SearchCriteria criteria, List<MusicFolder> musicFolders, | ||||||
|  |             IndexType indexType) throws ParseException, IOException { | ||||||
|  |         /* | ||||||
|  |          * Version.LUCENE_30 | ||||||
|  |          * It is a transient description and will be deleted when upgrading the version. | ||||||
|  |          * SearchService variables are not used because the reference direction conflicts. | ||||||
|  |          */ | ||||||
|  |         MultiFieldQueryParser queryParser = new MultiFieldQueryParser(Version.LUCENE_30, | ||||||
|  |                 indexType.getFields(), analyzerFactory.getQueryAnalyzer(), indexType.getBoosts()); | ||||||
|  | 
 | ||||||
|  |         BooleanQuery query = new BooleanQuery(); | ||||||
|  |         query.add(queryParser.parse(analyzeQuery(criteria.getQuery())), BooleanClause.Occur.MUST); | ||||||
|  |         List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>(); | ||||||
|  |         for (MusicFolder musicFolder : musicFolders) { | ||||||
|  |             if (indexType == ALBUM_ID3 || indexType == ARTIST_ID3) { | ||||||
|  |                 musicFolderQueries.add(new SpanTermQuery(new Term(FieldNames.FOLDER_ID, | ||||||
|  |                         NumericUtils.intToPrefixCoded(musicFolder.getId())))); | ||||||
|  |             } else { | ||||||
|  |                 musicFolderQueries.add(new SpanTermQuery( | ||||||
|  |                         new Term(FieldNames.FOLDER, musicFolder.getPath().getPath()))); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         query.add( | ||||||
|  |                 new SpanOrQuery( | ||||||
|  |                         musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])), | ||||||
|  |                 BooleanClause.Occur.MUST); | ||||||
|  |         return query; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Query generation expression extracted from | ||||||
|  |      * {@link org.airsonic.player.service.SearchService#getRandomSongs(RandomSearchCriteria)}. | ||||||
|  |      *  | ||||||
|  |      * @param criteria criteria | ||||||
|  |      * @return Query | ||||||
|  |      */ | ||||||
|  |     public Query getRandomSongs(RandomSearchCriteria criteria) { | ||||||
|  |         BooleanQuery query = new BooleanQuery(); | ||||||
|  |         query.add(new TermQuery( | ||||||
|  |                 new Term(FieldNames.MEDIA_TYPE, MediaFile.MediaType.MUSIC.name().toLowerCase())), | ||||||
|  |                 BooleanClause.Occur.MUST); | ||||||
|  |         if (criteria.getGenre() != null) { | ||||||
|  |             String genre = normalizeGenre(criteria.getGenre()); | ||||||
|  |             query.add(new TermQuery(new Term(FieldNames.GENRE, genre)), BooleanClause.Occur.MUST); | ||||||
|  |         } | ||||||
|  |         if (criteria.getFromYear() != null || criteria.getToYear() != null) { | ||||||
|  |             NumericRangeQuery<Integer> rangeQuery = NumericRangeQuery.newIntRange(FieldNames.YEAR, | ||||||
|  |                     criteria.getFromYear(), criteria.getToYear(), true, true); | ||||||
|  |             query.add(rangeQuery, BooleanClause.Occur.MUST); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>(); | ||||||
|  |         for (MusicFolder musicFolder : criteria.getMusicFolders()) { | ||||||
|  |             musicFolderQueries.add(new SpanTermQuery( | ||||||
|  |                     new Term(FieldNames.FOLDER, musicFolder.getPath().getPath()))); | ||||||
|  |         } | ||||||
|  |         query.add( | ||||||
|  |                 new SpanOrQuery( | ||||||
|  |                         musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])), | ||||||
|  |                 BooleanClause.Occur.MUST); | ||||||
|  |         return query; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Query generation expression extracted from | ||||||
|  |      * {@link org.airsonic.player.service.SearchService#searchByName( String, String, int, int, List, Class)}. | ||||||
|  |      *  | ||||||
|  |      * @param fieldName {@link FieldNames} | ||||||
|  |      * @return Query | ||||||
|  |      * @throws ParseException When parsing of QueryParser fails | ||||||
|  |      */ | ||||||
|  |     public Query searchByName(String fieldName, String name) throws ParseException { | ||||||
|  |         /* | ||||||
|  |          * Version.LUCENE_30 | ||||||
|  |          * It is a transient description and will be deleted when upgrading the version. | ||||||
|  |          * SearchService variables are not used because the reference direction conflicts. | ||||||
|  |          */ | ||||||
|  |         QueryParser queryParser = new QueryParser(Version.LUCENE_30, fieldName, | ||||||
|  |                 analyzerFactory.getQueryAnalyzer()); | ||||||
|  |         Query query = queryParser.parse(name + "*"); | ||||||
|  |         return query; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Query generation expression extracted from | ||||||
|  |      * {@link org.airsonic.player.service.SearchService#getRandomAlbums(int, List)}. | ||||||
|  |      *  | ||||||
|  |      * @param musicFolders musicFolders | ||||||
|  |      * @return Query | ||||||
|  |      */ | ||||||
|  |     public Query getRandomAlbums(List<MusicFolder> musicFolders) { | ||||||
|  |         List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>(); | ||||||
|  |         for (MusicFolder musicFolder : musicFolders) { | ||||||
|  |             musicFolderQueries.add(new SpanTermQuery( | ||||||
|  |                     new Term(FieldNames.FOLDER, musicFolder.getPath().getPath()))); | ||||||
|  |         } | ||||||
|  |         Query query = new SpanOrQuery( | ||||||
|  |                 musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])); | ||||||
|  |         return query; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Query generation expression extracted from | ||||||
|  |      * {@link org.airsonic.player.service.SearchService#getRandomAlbumsId3(int, List)}. | ||||||
|  |      *  | ||||||
|  |      * @param musicFolders musicFolders | ||||||
|  |      * @return Query | ||||||
|  |      */ | ||||||
|  |     public Query getRandomAlbumsId3(List<MusicFolder> musicFolders) { | ||||||
|  | 
 | ||||||
|  |         List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>(); | ||||||
|  |         for (MusicFolder musicFolder : musicFolders) { | ||||||
|  |             musicFolderQueries.add(new SpanTermQuery(new Term(FieldNames.FOLDER_ID, | ||||||
|  |                     NumericUtils.intToPrefixCoded(musicFolder.getId())))); | ||||||
|  |         } | ||||||
|  |         Query query = new SpanOrQuery( | ||||||
|  |                 musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])); | ||||||
|  |         return query; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,298 @@ | |||||||
|  | /* | ||||||
|  |  This file is part of Airsonic. | ||||||
|  | 
 | ||||||
|  |  Airsonic is free software: you can redistribute it and/or modify | ||||||
|  |  it under the terms of the GNU General Public License as published by | ||||||
|  |  the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |  Airsonic 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 General Public License for more details. | ||||||
|  | 
 | ||||||
|  |  You should have received a copy of the GNU General Public License | ||||||
|  |  along with Airsonic.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | 
 | ||||||
|  |  Copyright 2016 (C) Airsonic Authors | ||||||
|  |  Based upon Subsonic, Copyright 2009 (C) Sindre Mehus | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package org.airsonic.player.service.search; | ||||||
|  | 
 | ||||||
|  | import org.airsonic.player.domain.*; | ||||||
|  | import org.airsonic.player.service.SearchService; | ||||||
|  | import org.airsonic.player.util.FileUtil; | ||||||
|  | import org.apache.lucene.document.Document; | ||||||
|  | import org.apache.lucene.index.IndexReader; | ||||||
|  | import org.apache.lucene.index.IndexWriter; | ||||||
|  | import org.apache.lucene.queryParser.ParseException; | ||||||
|  | import org.apache.lucene.search.*; | ||||||
|  | import org.apache.lucene.store.Directory; | ||||||
|  | import org.apache.lucene.store.FSDirectory; | ||||||
|  | import org.checkerframework.checker.nullness.qual.Nullable; | ||||||
|  | import org.slf4j.Logger; | ||||||
|  | import org.slf4j.LoggerFactory; | ||||||
|  | import org.springframework.beans.factory.annotation.Autowired; | ||||||
|  | import org.springframework.stereotype.Service; | ||||||
|  | 
 | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.util.*; | ||||||
|  | import java.util.function.BiConsumer; | ||||||
|  | import java.util.stream.Collectors; | ||||||
|  | 
 | ||||||
|  | import static org.airsonic.player.service.search.IndexType.*; | ||||||
|  | import static org.springframework.util.ObjectUtils.isEmpty; | ||||||
|  | 
 | ||||||
|  | @Service | ||||||
|  | public class SearchServiceImpl implements SearchService { | ||||||
|  | 
 | ||||||
|  |     private static final Logger LOG = LoggerFactory.getLogger(SearchServiceImpl.class); | ||||||
|  | 
 | ||||||
|  |     @Autowired | ||||||
|  |     private QueryFactory           queryFactory; | ||||||
|  |     @Autowired | ||||||
|  |     private IndexManager           indexManager; | ||||||
|  |     @Autowired | ||||||
|  |     private SearchServiceUtilities util; | ||||||
|  | 
 | ||||||
|  |     // TODO Should be changed to SecureRandom?
 | ||||||
|  |     private final Random random = new Random(System.currentTimeMillis()); | ||||||
|  | 
 | ||||||
|  |     public SearchServiceImpl() { | ||||||
|  |         removeLocks(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void startIndexing() { | ||||||
|  |         indexManager.startIndexing(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void index(MediaFile mediaFile) { | ||||||
|  |         indexManager.index(mediaFile); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void index(Artist artist, MusicFolder musicFolder) { | ||||||
|  |         indexManager.index(artist, musicFolder); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void index(Album album) { | ||||||
|  |         indexManager.index(album); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void stopIndexing() { | ||||||
|  |         indexManager.stopIndexing(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public SearchResult search(SearchCriteria criteria, List<MusicFolder> musicFolders, | ||||||
|  |             IndexType indexType) { | ||||||
|  | 
 | ||||||
|  |         SearchResult result = new SearchResult(); | ||||||
|  |         int offset = criteria.getOffset(); | ||||||
|  |         int count = criteria.getCount(); | ||||||
|  |         result.setOffset(offset); | ||||||
|  | 
 | ||||||
|  |         if (count <= 0) | ||||||
|  |             return result; | ||||||
|  | 
 | ||||||
|  |         IndexReader reader = null; | ||||||
|  |         try { | ||||||
|  | 
 | ||||||
|  |             reader = indexManager.createIndexReader(indexType); | ||||||
|  |             Searcher searcher = new IndexSearcher(reader); | ||||||
|  |             Query query = queryFactory.search(criteria, musicFolders, indexType); | ||||||
|  | 
 | ||||||
|  |             TopDocs topDocs = searcher.search(query, null, offset + count); | ||||||
|  |             result.setTotalHits(topDocs.totalHits); | ||||||
|  |             int start = Math.min(offset, topDocs.totalHits); | ||||||
|  |             int end = Math.min(start + count, topDocs.totalHits); | ||||||
|  | 
 | ||||||
|  |             for (int i = start; i < end; i++) { | ||||||
|  |                 util.addIfAnyMatch(result, indexType, searcher.doc(topDocs.scoreDocs[i].doc)); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |         } catch (IOException | ParseException e) { | ||||||
|  |             LOG.error("Failed to execute Lucene search.", e); | ||||||
|  |         } finally { | ||||||
|  |             FileUtil.closeQuietly(reader); | ||||||
|  |         } | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Common processing of random method. | ||||||
|  |      *  | ||||||
|  |      * @param count Number of albums to return. | ||||||
|  |      * @param searcher | ||||||
|  |      * @param query | ||||||
|  |      * @param id2ListCallBack Callback to get D from id and store it in List | ||||||
|  |      * @return result | ||||||
|  |      * @throws IOException | ||||||
|  |      */ | ||||||
|  |     private final <D> List<D> createRandomDocsList( | ||||||
|  |             int count, Searcher searcher, Query query, BiConsumer<List<D>, Integer> id2ListCallBack) | ||||||
|  |             throws IOException { | ||||||
|  | 
 | ||||||
|  |         List<Integer> docs = Arrays | ||||||
|  |                 .stream(searcher.search(query, Integer.MAX_VALUE).scoreDocs) | ||||||
|  |                 .map(sd -> sd.doc) | ||||||
|  |                 .collect(Collectors.toList()); | ||||||
|  | 
 | ||||||
|  |         List<D> result = new ArrayList<>(); | ||||||
|  |         while (!docs.isEmpty() && result.size() < count) { | ||||||
|  |             int randomPos = random.nextInt(docs.size()); | ||||||
|  |             Document document = searcher.doc(docs.get(randomPos)); | ||||||
|  |             id2ListCallBack.accept(result, util.getId.apply(document)); | ||||||
|  |             docs.remove(randomPos); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public List<MediaFile> getRandomSongs(RandomSearchCriteria criteria) { | ||||||
|  | 
 | ||||||
|  |         IndexReader reader = null; | ||||||
|  |         try { | ||||||
|  |             reader = indexManager.createIndexReader(SONG); | ||||||
|  |             Searcher searcher = new IndexSearcher(reader); | ||||||
|  |             if (isEmpty(searcher)) { | ||||||
|  |                 // At first start
 | ||||||
|  |                 return Collections.emptyList(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             Query query = queryFactory.getRandomSongs(criteria); | ||||||
|  |             return createRandomDocsList(criteria.getCount(), searcher, query, | ||||||
|  |                     (dist, id) -> util.addIgnoreNull(dist, SONG, id)); | ||||||
|  | 
 | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             LOG.error("Failed to search or random songs.", e); | ||||||
|  |         } finally { | ||||||
|  |             FileUtil.closeQuietly(reader); | ||||||
|  |         } | ||||||
|  |         return Collections.emptyList(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public List<MediaFile> getRandomAlbums(int count, List<MusicFolder> musicFolders) { | ||||||
|  | 
 | ||||||
|  |         IndexReader reader = null; | ||||||
|  |         try { | ||||||
|  |             reader = indexManager.createIndexReader(ALBUM); | ||||||
|  |             Searcher searcher = new IndexSearcher(reader); | ||||||
|  |             if (isEmpty(searcher)) { | ||||||
|  |                 return Collections.emptyList(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             Query query = queryFactory.getRandomAlbums(musicFolders); | ||||||
|  |             return createRandomDocsList(count, searcher, query, | ||||||
|  |                     (dist, id) -> util.addIgnoreNull(dist, ALBUM, id)); | ||||||
|  | 
 | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             LOG.error("Failed to search for random albums.", e); | ||||||
|  |         } finally { | ||||||
|  |             FileUtil.closeQuietly(reader); | ||||||
|  |         } | ||||||
|  |         return Collections.emptyList(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public List<Album> getRandomAlbumsId3(int count, List<MusicFolder> musicFolders) { | ||||||
|  | 
 | ||||||
|  |         IndexReader reader = null; | ||||||
|  |         try { | ||||||
|  |             reader = indexManager.createIndexReader(ALBUM_ID3); | ||||||
|  |             Searcher searcher = new IndexSearcher(reader); | ||||||
|  |             if (isEmpty(searcher)) { | ||||||
|  |                 return Collections.emptyList(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             Query query = queryFactory.getRandomAlbumsId3(musicFolders); | ||||||
|  |             return createRandomDocsList(count, searcher, query, | ||||||
|  |                     (dist, id) -> util.addIgnoreNull(dist, ALBUM_ID3, id)); | ||||||
|  | 
 | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             LOG.error("Failed to search for random albums.", e); | ||||||
|  |         } finally { | ||||||
|  |             FileUtil.closeQuietly(reader); | ||||||
|  |         } | ||||||
|  |         return Collections.emptyList(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public <T> ParamSearchResult<T> searchByName(String name, int offset, int count, | ||||||
|  |             List<MusicFolder> folderList, Class<T> assignableClass) { | ||||||
|  | 
 | ||||||
|  |         // we only support album, artist, and song for now
 | ||||||
|  |         @Nullable | ||||||
|  |         IndexType indexType = util.getIndexType.apply(assignableClass); | ||||||
|  |         @Nullable | ||||||
|  |         String fieldName = util.getFieldName.apply(assignableClass); | ||||||
|  | 
 | ||||||
|  |         ParamSearchResult<T> result = new ParamSearchResult<T>(); | ||||||
|  |         result.setOffset(offset); | ||||||
|  | 
 | ||||||
|  |         if (isEmpty(indexType) || isEmpty(fieldName) || count <= 0) { | ||||||
|  |             return result; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         IndexReader reader = null; | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             reader = indexManager.createIndexReader(indexType); | ||||||
|  |             Searcher searcher = new IndexSearcher(reader); | ||||||
|  |             Query query = queryFactory.searchByName(fieldName, name); | ||||||
|  | 
 | ||||||
|  |             Sort sort = new Sort(new SortField(fieldName, SortField.STRING)); | ||||||
|  |             TopDocs topDocs = searcher.search(query, null, offset + count, sort); | ||||||
|  | 
 | ||||||
|  |             result.setTotalHits(topDocs.totalHits); | ||||||
|  |             int start = Math.min(offset, topDocs.totalHits); | ||||||
|  |             int end = Math.min(start + count, topDocs.totalHits); | ||||||
|  | 
 | ||||||
|  |             for (int i = start; i < end; i++) { | ||||||
|  |                 Document doc = searcher.doc(topDocs.scoreDocs[i].doc); | ||||||
|  |                 util.addIgnoreNull(result, indexType, util.getId.apply(doc), assignableClass); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |         } catch (IOException | ParseException e) { | ||||||
|  |             LOG.error("Failed to execute Lucene search.", e); | ||||||
|  |         } finally { | ||||||
|  |             FileUtil.closeQuietly(reader); | ||||||
|  |         } | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Locks are managed automatically by the framework. | ||||||
|  |      *  | ||||||
|  |      * @deprecated It becomes unnecessary at the time of version upgrade. | ||||||
|  |      */ | ||||||
|  |     @Deprecated | ||||||
|  |     public void removeLocks() { | ||||||
|  |         for (IndexType indexType : IndexType.values()) { | ||||||
|  |             Directory dir = null; | ||||||
|  |             try { | ||||||
|  |                 /* | ||||||
|  |                  * Static access to the accompanying method is performed as a transition period. | ||||||
|  |                  * (Unnecessary processing after updating Lucene.) | ||||||
|  |                  */ | ||||||
|  |                 dir = FSDirectory.open(IndexManager.getIndexDirectory(indexType)); | ||||||
|  |                 if (IndexWriter.isLocked(dir)) { | ||||||
|  |                     IndexWriter.unlock(dir); | ||||||
|  |                     LOG.info("Removed Lucene lock file in " + dir); | ||||||
|  |                 } | ||||||
|  |             } catch (Exception x) { | ||||||
|  |                 LOG.warn("Failed to remove Lucene lock file in " + dir, x); | ||||||
|  |             } finally { | ||||||
|  |                 FileUtil.closeQuietly(dir); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,200 @@ | |||||||
|  | /* | ||||||
|  |  This file is part of Airsonic. | ||||||
|  | 
 | ||||||
|  |  Airsonic is free software: you can redistribute it and/or modify | ||||||
|  |  it under the terms of the GNU General Public License as published by | ||||||
|  |  the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |  Airsonic 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 General Public License for more details. | ||||||
|  | 
 | ||||||
|  |  You should have received a copy of the GNU General Public License | ||||||
|  |  along with Airsonic.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | 
 | ||||||
|  |  Copyright 2016 (C) Airsonic Authors | ||||||
|  |  Based upon Subsonic, Copyright 2009 (C) Sindre Mehus | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package org.airsonic.player.service.search; | ||||||
|  | 
 | ||||||
|  | import org.airsonic.player.dao.AlbumDao; | ||||||
|  | import org.airsonic.player.dao.ArtistDao; | ||||||
|  | import org.airsonic.player.domain.Album; | ||||||
|  | import org.airsonic.player.domain.Artist; | ||||||
|  | import org.airsonic.player.domain.MediaFile; | ||||||
|  | import org.airsonic.player.domain.ParamSearchResult; | ||||||
|  | import org.airsonic.player.domain.SearchResult; | ||||||
|  | import org.airsonic.player.service.MediaFileService; | ||||||
|  | import org.airsonic.player.service.SettingsService; | ||||||
|  | import org.apache.commons.collections.CollectionUtils; | ||||||
|  | import org.apache.lucene.document.Document; | ||||||
|  | import org.apache.lucene.index.Term; | ||||||
|  | import org.checkerframework.checker.nullness.qual.Nullable; | ||||||
|  | import org.springframework.beans.factory.annotation.Autowired; | ||||||
|  | import org.springframework.stereotype.Component; | ||||||
|  | 
 | ||||||
|  | import java.io.File; | ||||||
|  | import java.util.Collection; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.function.BiConsumer; | ||||||
|  | import java.util.function.BiFunction; | ||||||
|  | import java.util.function.Function; | ||||||
|  | 
 | ||||||
|  | import static org.springframework.util.ObjectUtils.isEmpty; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Termination used by SearchService. | ||||||
|  |  *  | ||||||
|  |  * Since SearchService operates as a proxy for storage (DB) using lucene, | ||||||
|  |  * there are many redundant descriptions different from essential data processing. | ||||||
|  |  * This class is a transfer class for saving those redundant descriptions. | ||||||
|  |  *  | ||||||
|  |  * Exception handling is not termination, | ||||||
|  |  * so do not include exception handling in this class. | ||||||
|  |  */ | ||||||
|  | @Component | ||||||
|  | public class SearchServiceUtilities { | ||||||
|  | 
 | ||||||
|  |     /* Search by id only. */ | ||||||
|  |     @Autowired | ||||||
|  |     private ArtistDao artistDao; | ||||||
|  | 
 | ||||||
|  |     /* Search by id only. */ | ||||||
|  |     @Autowired | ||||||
|  |     private AlbumDao albumDao; | ||||||
|  | 
 | ||||||
|  |     /* | ||||||
|  |      * Search by id only. | ||||||
|  |      * Although there is no influence at present, | ||||||
|  |      * mediaFileService has a caching mechanism. | ||||||
|  |      * Service is used instead of Dao until you are sure you need to use mediaFileDao. | ||||||
|  |      */ | ||||||
|  |     @Autowired | ||||||
|  |     private MediaFileService mediaFileService; | ||||||
|  | 
 | ||||||
|  |     public final Function<Long, Integer> round = (i) -> { | ||||||
|  |         // return
 | ||||||
|  |         // NumericUtils.floatToSortableInt(i);
 | ||||||
|  |         return i.intValue(); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     public final Function<Document, Integer> getId = d -> { | ||||||
|  |         return Integer.valueOf(d.get(FieldNames.ID)); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     public final BiConsumer<List<MediaFile>, Integer> addMediaFileIfAnyMatch = (dist, id) -> { | ||||||
|  |         if (!dist.stream().anyMatch(m -> id == m.getId())) { | ||||||
|  |             MediaFile mediaFile = mediaFileService.getMediaFile(id); | ||||||
|  |             if (!isEmpty(mediaFile)) { | ||||||
|  |                 dist.add(mediaFile); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     public final BiConsumer<List<Artist>, Integer> addArtistId3IfAnyMatch = (dist, id) -> { | ||||||
|  |         if (!dist.stream().anyMatch(a -> id == a.getId())) { | ||||||
|  |             Artist artist = artistDao.getArtist(id); | ||||||
|  |             if (!isEmpty(artist)) { | ||||||
|  |                 dist.add(artist); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     public final Function<Class<?>, @Nullable IndexType> getIndexType = (assignableClass) -> { | ||||||
|  |         IndexType indexType = null; | ||||||
|  |         if (assignableClass.isAssignableFrom(Album.class)) { | ||||||
|  |             indexType = IndexType.ALBUM_ID3; | ||||||
|  |         } else if (assignableClass.isAssignableFrom(Artist.class)) { | ||||||
|  |             indexType = IndexType.ARTIST_ID3; | ||||||
|  |         } else if (assignableClass.isAssignableFrom(MediaFile.class)) { | ||||||
|  |             indexType = IndexType.SONG; | ||||||
|  |         } | ||||||
|  |         return indexType; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     public final Function<Class<?>, @Nullable String> getFieldName = (assignableClass) -> { | ||||||
|  |         String fieldName = null; | ||||||
|  |         if (assignableClass.isAssignableFrom(Album.class)) { | ||||||
|  |             fieldName = FieldNames.ALBUM; | ||||||
|  |         } else if (assignableClass.isAssignableFrom(Artist.class)) { | ||||||
|  |             fieldName = FieldNames.ARTIST; | ||||||
|  |         } else if (assignableClass.isAssignableFrom(MediaFile.class)) { | ||||||
|  |             fieldName = FieldNames.TITLE; | ||||||
|  |         } | ||||||
|  |         return fieldName; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     public final BiConsumer<List<Album>, Integer> addAlbumId3IfAnyMatch = (dist, subjectId) -> { | ||||||
|  |         if (!dist.stream().anyMatch(a -> subjectId == a.getId())) { | ||||||
|  |             Album album = albumDao.getAlbum(subjectId); | ||||||
|  |             if (!isEmpty(album)) { | ||||||
|  |                 dist.add(album); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     private final Function<String, File> getRootDirectory = (version) -> { | ||||||
|  |         return new File(SettingsService.getAirsonicHome(), version); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     public final BiFunction<String, IndexType, File> getDirectory = (version, indexType) -> { | ||||||
|  |         return new File(getRootDirectory.apply(version), indexType.toString().toLowerCase()); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     public final Term createPrimarykey(Album album) { | ||||||
|  |         return new Term(FieldNames.ID, Integer.toString(album.getId())); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     public final Term createPrimarykey(Artist artist) { | ||||||
|  |         return new Term(FieldNames.ID, Integer.toString(artist.getId())); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     public final Term createPrimarykey(MediaFile mediaFile) { | ||||||
|  |         return new Term(FieldNames.ID, Integer.toString(mediaFile.getId())); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     public final boolean addIgnoreNull(Collection<?> collection, Object object) { | ||||||
|  |         return CollectionUtils.addIgnoreNull(collection, object); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public final boolean addIgnoreNull(Collection<?> collection, IndexType indexType, | ||||||
|  |             int subjectId) { | ||||||
|  |         if (indexType == IndexType.ALBUM | indexType == IndexType.SONG) { | ||||||
|  |             return addIgnoreNull(collection, mediaFileService.getMediaFile(subjectId)); | ||||||
|  |         } else if (indexType == IndexType.ALBUM_ID3) { | ||||||
|  |             return addIgnoreNull(collection, albumDao.getAlbum(subjectId)); | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public final <T> void addIgnoreNull(ParamSearchResult<T> dist, IndexType indexType, | ||||||
|  |             int subjectId, Class<T> subjectClass) { | ||||||
|  |         if (indexType == IndexType.SONG) { | ||||||
|  |             MediaFile mediaFile = mediaFileService.getMediaFile(subjectId); | ||||||
|  |             addIgnoreNull(dist.getItems(), subjectClass.cast(mediaFile)); | ||||||
|  |         } else if (indexType == IndexType.ARTIST_ID3) { | ||||||
|  |             Artist artist = artistDao.getArtist(subjectId); | ||||||
|  |             addIgnoreNull(dist.getItems(), subjectClass.cast(artist)); | ||||||
|  |         } else if (indexType == IndexType.ALBUM_ID3) { | ||||||
|  |             Album album = albumDao.getAlbum(subjectId); | ||||||
|  |             addIgnoreNull(dist.getItems(), subjectClass.cast(album)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public final void addIfAnyMatch(SearchResult dist, IndexType subjectIndexType, | ||||||
|  |             Document subject) { | ||||||
|  |         int documentId = getId.apply(subject); | ||||||
|  |         if (subjectIndexType == IndexType.ARTIST | subjectIndexType == IndexType.ALBUM | ||||||
|  |                 | subjectIndexType == IndexType.SONG) { | ||||||
|  |             addMediaFileIfAnyMatch.accept(dist.getMediaFiles(), documentId); | ||||||
|  |         } else if (subjectIndexType == IndexType.ARTIST_ID3) { | ||||||
|  |             addArtistId3IfAnyMatch.accept(dist.getArtists(), documentId); | ||||||
|  |         } else if (subjectIndexType == IndexType.ALBUM_ID3) { | ||||||
|  |             addAlbumId3IfAnyMatch.accept(dist.getAlbums(), documentId); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,256 @@ | |||||||
|  | 
 | ||||||
|  | package org.airsonic.player.service.search; | ||||||
|  | 
 | ||||||
|  | import static org.junit.Assert.assertEquals; | ||||||
|  | 
 | ||||||
|  | import org.airsonic.player.domain.MusicFolder; | ||||||
|  | import org.airsonic.player.domain.RandomSearchCriteria; | ||||||
|  | import org.airsonic.player.domain.SearchCriteria; | ||||||
|  | import org.airsonic.player.util.HomeRule; | ||||||
|  | import org.apache.commons.lang.builder.ToStringBuilder; | ||||||
|  | import org.apache.lucene.queryParser.ParseException; | ||||||
|  | import org.apache.lucene.search.Query; | ||||||
|  | import org.apache.lucene.util.NumericUtils; | ||||||
|  | import org.junit.ClassRule; | ||||||
|  | import org.junit.Rule; | ||||||
|  | import org.junit.Test; | ||||||
|  | import org.junit.runner.Description; | ||||||
|  | import org.junit.runners.model.Statement; | ||||||
|  | import org.springframework.beans.factory.annotation.Autowired; | ||||||
|  | import org.springframework.test.annotation.DirtiesContext; | ||||||
|  | import org.springframework.test.context.ContextConfiguration; | ||||||
|  | import org.springframework.test.context.junit4.rules.SpringClassRule; | ||||||
|  | import org.springframework.test.context.junit4.rules.SpringMethodRule; | ||||||
|  | 
 | ||||||
|  | import java.io.File; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Test case for QueryFactory. | ||||||
|  |  * These cases have the purpose of observing the current situation | ||||||
|  |  * and observing the impact of upgrading Lucene. | ||||||
|  |  */ | ||||||
|  | @ContextConfiguration( | ||||||
|  |         locations = { | ||||||
|  |                 "/applicationContext-service.xml", | ||||||
|  |                 "/applicationContext-cache.xml", | ||||||
|  |                 "/applicationContext-testdb.xml", | ||||||
|  |                 "/applicationContext-mockSonos.xml" }) | ||||||
|  | @DirtiesContext( | ||||||
|  |     classMode = DirtiesContext.ClassMode.AFTER_CLASS) | ||||||
|  | public class QueryFactoryTestCase { | ||||||
|  | 
 | ||||||
|  |     @ClassRule | ||||||
|  |     public static final SpringClassRule classRule = new SpringClassRule() { | ||||||
|  |         HomeRule homeRule = new HomeRule(); | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public Statement apply(Statement base, Description description) { | ||||||
|  |             Statement spring = super.apply(base, description); | ||||||
|  |             return homeRule.apply(spring, description); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     @Rule | ||||||
|  |     public final SpringMethodRule springMethodRule = new SpringMethodRule(); | ||||||
|  | 
 | ||||||
|  |     @Autowired | ||||||
|  |     private QueryFactory queryFactory; | ||||||
|  | 
 | ||||||
|  |     private static final String QUERY_ENG_ONLY = "ABC DEF"; | ||||||
|  | 
 | ||||||
|  |     private static final String SEPA = System.getProperty("file.separator"); | ||||||
|  | 
 | ||||||
|  |     private static final String PATH1 = SEPA + "var" + SEPA + "music1"; | ||||||
|  |     private static final String PATH2 = SEPA + "var" + SEPA + "music2"; | ||||||
|  | 
 | ||||||
|  |     private static final int FID1 = 10; | ||||||
|  |     private static final int FID2 = 20; | ||||||
|  | 
 | ||||||
|  |     private static final MusicFolder MUSIC_FOLDER1 = | ||||||
|  |             new MusicFolder(Integer.valueOf(FID1), new File(PATH1), "music1", true, new java.util.Date()); | ||||||
|  |     private static final MusicFolder MUSIC_FOLDER2 =  | ||||||
|  |             new MusicFolder(Integer.valueOf(FID2), new File(PATH2), "music2", true, new java.util.Date()); | ||||||
|  | 
 | ||||||
|  |     private static final List<MusicFolder> SINGLE_FOLDERS = Arrays.asList(MUSIC_FOLDER1); | ||||||
|  |     private static final List<MusicFolder> MULTI_FOLDERS  = Arrays.asList(MUSIC_FOLDER1, MUSIC_FOLDER2); | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void testSearchArtist() throws ParseException, IOException { | ||||||
|  |         SearchCriteria criteria = new SearchCriteria(); | ||||||
|  |         criteria.setOffset(10); | ||||||
|  |         criteria.setCount(Integer.MAX_VALUE); | ||||||
|  |         criteria.setQuery(QUERY_ENG_ONLY); | ||||||
|  | 
 | ||||||
|  |         Query query = queryFactory.search(criteria, SINGLE_FOLDERS, IndexType.ARTIST); | ||||||
|  |         assertEquals("SearchArtist", | ||||||
|  |                 "+((artist:abc* folder:abc*) (artist:def* folder:def*)) +spanOr([folder:" + PATH1 + "])", | ||||||
|  |                 query.toString()); | ||||||
|  | 
 | ||||||
|  |         query = queryFactory.search(criteria, MULTI_FOLDERS, IndexType.ARTIST); | ||||||
|  |         assertEquals("SearchArtist", "+((artist:abc* folder:abc*) (artist:def* folder:def*)) +spanOr([folder:" + PATH1 | ||||||
|  |                 + ", folder:" + PATH2 + "])", query.toString()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void testSearchAlbum() throws ParseException, IOException { | ||||||
|  |         SearchCriteria criteria = new SearchCriteria(); | ||||||
|  |         criteria.setOffset(10); | ||||||
|  |         criteria.setCount(Integer.MAX_VALUE); | ||||||
|  |         criteria.setQuery(QUERY_ENG_ONLY); | ||||||
|  | 
 | ||||||
|  |         Query query = queryFactory.search(criteria, SINGLE_FOLDERS, IndexType.ALBUM); | ||||||
|  |         assertEquals("SearchAlbum", | ||||||
|  |                 "+((album:abc* artist:abc* folder:abc*) (album:def* artist:def* folder:def*)) +spanOr([folder:" + PATH1 | ||||||
|  |                         + "])", | ||||||
|  |                 query.toString()); | ||||||
|  | 
 | ||||||
|  |         query = queryFactory.search(criteria, MULTI_FOLDERS, IndexType.ALBUM); | ||||||
|  |         assertEquals("SearchAlbum", | ||||||
|  |                 "+((album:abc* artist:abc* folder:abc*) (album:def* artist:def* folder:def*)) +spanOr([folder:" + PATH1 | ||||||
|  |                         + ", folder:" + PATH2 + "])", | ||||||
|  |                 query.toString()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void testSearchSong() throws ParseException, IOException { | ||||||
|  |         SearchCriteria criteria = new SearchCriteria(); | ||||||
|  |         criteria.setOffset(10); | ||||||
|  |         criteria.setCount(Integer.MAX_VALUE); | ||||||
|  |         criteria.setQuery(QUERY_ENG_ONLY); | ||||||
|  | 
 | ||||||
|  |         Query query = queryFactory.search(criteria, SINGLE_FOLDERS, IndexType.SONG); | ||||||
|  |         assertEquals("SearchSong", | ||||||
|  |                 "+((title:abc* artist:abc*) (title:def* artist:def*)) +spanOr([folder:" + PATH1 + "])", | ||||||
|  |                 query.toString()); | ||||||
|  | 
 | ||||||
|  |         query = queryFactory.search(criteria, MULTI_FOLDERS, IndexType.SONG); | ||||||
|  |         assertEquals("SearchSong", "+((title:abc* artist:abc*) (title:def* artist:def*)) +spanOr([folder:" + PATH1 | ||||||
|  |                 + ", folder:" + PATH2 + "])", query.toString()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void testSearchArtistId3() throws ParseException, IOException { | ||||||
|  |         SearchCriteria criteria = new SearchCriteria(); | ||||||
|  |         criteria.setOffset(10); | ||||||
|  |         criteria.setCount(Integer.MAX_VALUE); | ||||||
|  |         criteria.setQuery(QUERY_ENG_ONLY); | ||||||
|  | 
 | ||||||
|  |         Query query = queryFactory.search(criteria, SINGLE_FOLDERS, IndexType.ARTIST_ID3); | ||||||
|  |         assertEquals("SearchSong", "+((artist:abc*) (artist:def*)) +spanOr([folderId:" | ||||||
|  |                 + NumericUtils.intToPrefixCoded(FID1) + "])", query.toString()); | ||||||
|  | 
 | ||||||
|  |         query = queryFactory.search(criteria, MULTI_FOLDERS, IndexType.ARTIST_ID3); | ||||||
|  |         assertEquals("SearchSong", | ||||||
|  |                 "+((artist:abc*) (artist:def*)) +spanOr([folderId:" + NumericUtils.intToPrefixCoded(FID1) | ||||||
|  |                         + ", folderId:" + NumericUtils.intToPrefixCoded(FID2) + "])", | ||||||
|  |                 query.toString()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void testSearchAlbumId3() throws ParseException, IOException { | ||||||
|  |         SearchCriteria criteria = new SearchCriteria(); | ||||||
|  |         criteria.setOffset(10); | ||||||
|  |         criteria.setCount(Integer.MAX_VALUE); | ||||||
|  |         criteria.setQuery(QUERY_ENG_ONLY); | ||||||
|  | 
 | ||||||
|  |         Query query = queryFactory.search(criteria, SINGLE_FOLDERS, IndexType.ALBUM_ID3); | ||||||
|  |         assertEquals( | ||||||
|  |                 "SearchAlbumId3", "+((album:abc* artist:abc* folderId:abc*) (album:def* artist:def* folderId:def*)) " | ||||||
|  |                         + "+spanOr([folderId:" + NumericUtils.intToPrefixCoded(FID1) + "])", | ||||||
|  |                 query.toString()); | ||||||
|  | 
 | ||||||
|  |         query = queryFactory.search(criteria, MULTI_FOLDERS, IndexType.ALBUM_ID3); | ||||||
|  |         assertEquals("SearchAlbumId3", | ||||||
|  |                 "+((album:abc* artist:abc* folderId:abc*) (album:def* artist:def* folderId:def*)) +spanOr([folderId:" | ||||||
|  |                         + NumericUtils.intToPrefixCoded(FID1) + ", folderId:" | ||||||
|  |                         + NumericUtils.intToPrefixCoded(FID2) + "])", | ||||||
|  |                 query.toString()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void testSearchByNameArtist() throws ParseException { | ||||||
|  |         Query query = queryFactory.searchByName(FieldNames.ARTIST, QUERY_ENG_ONLY); | ||||||
|  |         assertEquals("SearchByNameArtist", "artist:abc artist:def*", query.toString()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void testSearchByNameAlbum() throws ParseException { | ||||||
|  |         Query query = queryFactory.searchByName(FieldNames.ALBUM, QUERY_ENG_ONLY); | ||||||
|  |         assertEquals("SearchByNameAlbum", "album:abc album:def*", query.toString()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void testSearchByNameTitle() throws ParseException { | ||||||
|  |         Query query = queryFactory.searchByName(FieldNames.TITLE, QUERY_ENG_ONLY); | ||||||
|  |         assertEquals("SearchByNameTitle", "title:abc title:def*", query.toString()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void testGetRandomSongs() { | ||||||
|  |         RandomSearchCriteria criteria = new RandomSearchCriteria(50, "Classic Rock", | ||||||
|  |                 Integer.valueOf(1900), Integer.valueOf(2000), SINGLE_FOLDERS); | ||||||
|  | 
 | ||||||
|  |         Query query = queryFactory.getRandomSongs(criteria); | ||||||
|  |         assertEquals(ToStringBuilder.reflectionToString(criteria), | ||||||
|  |                 "+mediaType:music +genre:classicrock +year:[1900 TO 2000] +spanOr([folder:" + PATH1 + "])", | ||||||
|  |                 query.toString()); | ||||||
|  | 
 | ||||||
|  |         criteria = new RandomSearchCriteria(50, "Classic Rock", Integer.valueOf(1900), | ||||||
|  |                 Integer.valueOf(2000), MULTI_FOLDERS); | ||||||
|  |         query = queryFactory.getRandomSongs(criteria); | ||||||
|  |         assertEquals(ToStringBuilder.reflectionToString(criteria), | ||||||
|  |                 "+mediaType:music +genre:classicrock +year:[1900 TO 2000] +spanOr([folder:" + PATH1 + ", folder:" + PATH2 | ||||||
|  |                         + "])", | ||||||
|  |                 query.toString()); | ||||||
|  | 
 | ||||||
|  |         criteria = new RandomSearchCriteria(50, "Classic Rock", null, null, MULTI_FOLDERS); | ||||||
|  |         query = queryFactory.getRandomSongs(criteria); | ||||||
|  |         assertEquals(ToStringBuilder.reflectionToString(criteria), | ||||||
|  |                 "+mediaType:music +genre:classicrock +spanOr([folder:" + PATH1 + ", folder:" + PATH2 + "])", | ||||||
|  |                 query.toString()); | ||||||
|  | 
 | ||||||
|  |         criteria = new RandomSearchCriteria(50, "Classic Rock", Integer.valueOf(1900), null, | ||||||
|  |                 MULTI_FOLDERS); | ||||||
|  |         query = queryFactory.getRandomSongs(criteria); | ||||||
|  |         assertEquals(ToStringBuilder.reflectionToString(criteria), | ||||||
|  |                 "+mediaType:music +genre:classicrock +year:[1900 TO *] +spanOr([folder:" + PATH1 + ", folder:" + PATH2 | ||||||
|  |                         + "])", | ||||||
|  |                 query.toString()); | ||||||
|  | 
 | ||||||
|  |         criteria = new RandomSearchCriteria(50, "Classic Rock", null, Integer.valueOf(2000), | ||||||
|  |                 MULTI_FOLDERS); | ||||||
|  |         query = queryFactory.getRandomSongs(criteria); | ||||||
|  |         assertEquals(ToStringBuilder.reflectionToString(criteria), | ||||||
|  |                 "+mediaType:music +genre:classicrock +year:[* TO 2000] +spanOr([folder:" + PATH1 + ", folder:" + PATH2 | ||||||
|  |                         + "])", | ||||||
|  |                 query.toString()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void testGetRandomAlbums() { | ||||||
|  |         Query query = queryFactory.getRandomAlbums(SINGLE_FOLDERS); | ||||||
|  |         assertEquals(ToStringBuilder.reflectionToString(SINGLE_FOLDERS), | ||||||
|  |                 "spanOr([folder:" + PATH1 + "])", query.toString()); | ||||||
|  | 
 | ||||||
|  |         query = queryFactory.getRandomAlbums(MULTI_FOLDERS); | ||||||
|  |         assertEquals(ToStringBuilder.reflectionToString(MULTI_FOLDERS), | ||||||
|  |                 "spanOr([folder:" + PATH1 + ", folder:" + PATH2 + "])", query.toString()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void testGetRandomAlbumsId3() { | ||||||
|  |         Query query = queryFactory.getRandomAlbumsId3(SINGLE_FOLDERS); | ||||||
|  |         assertEquals(ToStringBuilder.reflectionToString(SINGLE_FOLDERS), | ||||||
|  |                 "spanOr([folderId:" + NumericUtils.intToPrefixCoded(FID1) + "])", query.toString()); | ||||||
|  | 
 | ||||||
|  |         query = queryFactory.getRandomAlbumsId3(MULTI_FOLDERS); | ||||||
|  |         assertEquals(ToStringBuilder.reflectionToString(MULTI_FOLDERS), | ||||||
|  |                 "spanOr([folderId:" + NumericUtils.intToPrefixCoded(FID1) + ", folderId:" | ||||||
|  |                         + NumericUtils.intToPrefixCoded(FID2) + "])", | ||||||
|  |                 query.toString()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,466 @@ | |||||||
|  | 
 | ||||||
|  | package org.airsonic.player.service.search; | ||||||
|  | 
 | ||||||
|  | import com.codahale.metrics.ConsoleReporter; | ||||||
|  | import com.codahale.metrics.MetricRegistry; | ||||||
|  | import com.codahale.metrics.Timer; | ||||||
|  | 
 | ||||||
|  | import java.io.File; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.Random; | ||||||
|  | import java.util.concurrent.TimeUnit; | ||||||
|  | 
 | ||||||
|  | import org.airsonic.player.TestCaseUtils; | ||||||
|  | import org.airsonic.player.dao.AlbumDao; | ||||||
|  | import org.airsonic.player.dao.DaoHelper; | ||||||
|  | import org.airsonic.player.dao.MediaFileDao; | ||||||
|  | import org.airsonic.player.dao.MusicFolderDao; | ||||||
|  | import org.airsonic.player.dao.MusicFolderTestData; | ||||||
|  | import org.airsonic.player.domain.Album; | ||||||
|  | import org.airsonic.player.domain.Artist; | ||||||
|  | import org.airsonic.player.domain.MediaFile; | ||||||
|  | import org.airsonic.player.domain.MediaFile.MediaType; | ||||||
|  | import org.airsonic.player.domain.MusicFolder; | ||||||
|  | import org.airsonic.player.domain.ParamSearchResult; | ||||||
|  | import org.airsonic.player.domain.RandomSearchCriteria; | ||||||
|  | import org.airsonic.player.domain.SearchCriteria; | ||||||
|  | import org.airsonic.player.domain.SearchResult; | ||||||
|  | import org.airsonic.player.service.MediaScannerService; | ||||||
|  | import org.airsonic.player.service.SearchService; | ||||||
|  | import org.airsonic.player.service.SettingsService; | ||||||
|  | import org.airsonic.player.service.search.IndexType; | ||||||
|  | import org.airsonic.player.util.HomeRule; | ||||||
|  | import org.junit.Assert; | ||||||
|  | import org.junit.Before; | ||||||
|  | import org.junit.ClassRule; | ||||||
|  | import org.junit.Rule; | ||||||
|  | import org.junit.Test; | ||||||
|  | import org.junit.rules.TemporaryFolder; | ||||||
|  | import org.junit.runner.Description; | ||||||
|  | import org.junit.runners.model.Statement; | ||||||
|  | import org.springframework.beans.factory.annotation.Autowired; | ||||||
|  | import org.springframework.core.io.ResourceLoader; | ||||||
|  | import org.springframework.test.annotation.DirtiesContext; | ||||||
|  | import org.springframework.test.context.ContextConfiguration; | ||||||
|  | import org.springframework.test.context.junit4.rules.SpringClassRule; | ||||||
|  | import org.springframework.test.context.junit4.rules.SpringMethodRule; | ||||||
|  | import org.subsonic.restapi.ArtistID3; | ||||||
|  | 
 | ||||||
|  | @ContextConfiguration( | ||||||
|  |         locations = { | ||||||
|  |                 "/applicationContext-service.xml", | ||||||
|  |                 "/applicationContext-cache.xml", | ||||||
|  |                 "/applicationContext-testdb.xml", | ||||||
|  |                 "/applicationContext-mockSonos.xml" }) | ||||||
|  | @DirtiesContext( | ||||||
|  |         classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) | ||||||
|  | public class SearchServiceTestCase { | ||||||
|  | 
 | ||||||
|  |     @ClassRule | ||||||
|  |     public static final SpringClassRule classRule = new SpringClassRule() { | ||||||
|  |         HomeRule homeRule = new HomeRule(); | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public Statement apply(Statement base, Description description) { | ||||||
|  |             Statement spring = super.apply(base, description); | ||||||
|  |             return homeRule.apply(spring, description); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     @Rule | ||||||
|  |     public final SpringMethodRule springMethodRule = new SpringMethodRule(); | ||||||
|  | 
 | ||||||
|  |     private final MetricRegistry metrics = new MetricRegistry(); | ||||||
|  | 
 | ||||||
|  |     @Autowired | ||||||
|  |     private MediaScannerService mediaScannerService; | ||||||
|  | 
 | ||||||
|  |     @Autowired | ||||||
|  |     private MediaFileDao mediaFileDao; | ||||||
|  | 
 | ||||||
|  |     @Autowired | ||||||
|  |     private MusicFolderDao musicFolderDao; | ||||||
|  | 
 | ||||||
|  |     @Autowired | ||||||
|  |     private DaoHelper daoHelper; | ||||||
|  | 
 | ||||||
|  |     @Autowired | ||||||
|  |     private AlbumDao albumDao; | ||||||
|  | 
 | ||||||
|  |     @Autowired | ||||||
|  |     private SearchService searchService; | ||||||
|  | 
 | ||||||
|  |     @Autowired | ||||||
|  |     private SettingsService settingsService; | ||||||
|  | 
 | ||||||
|  |     @Rule | ||||||
|  |     public TemporaryFolder temporaryFolder = new TemporaryFolder(); | ||||||
|  | 
 | ||||||
|  |     @Autowired | ||||||
|  |     ResourceLoader resourceLoader; | ||||||
|  | 
 | ||||||
|  |     @Before | ||||||
|  |     public void setup() throws Exception { | ||||||
|  |         populateDatabase(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static boolean dataBasePopulated; | ||||||
|  | 
 | ||||||
|  |     private int count = 1; | ||||||
|  | 
 | ||||||
|  |     /* | ||||||
|  |      * Cases susceptible to SerchService refactoring and version upgrades. | ||||||
|  |      * It is not exhaustive. | ||||||
|  |      */ | ||||||
|  |     private synchronized void populateDatabase() { | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          * It seems that there is a case that does not work well | ||||||
|  |          * if you test immediately after initialization in 1 method. | ||||||
|  |          * It may be improved later. | ||||||
|  |          */ | ||||||
|  |         try { | ||||||
|  |             Thread.sleep(300 * count++); | ||||||
|  |         } catch (InterruptedException e) { | ||||||
|  |             e.printStackTrace(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!dataBasePopulated) { | ||||||
|  | 
 | ||||||
|  |             MusicFolderTestData.getTestMusicFolders().forEach(musicFolderDao::createMusicFolder); | ||||||
|  |             settingsService.clearMusicFolderCache(); | ||||||
|  |             TestCaseUtils.execScan(mediaScannerService); | ||||||
|  |             System.out.println("--- Report of records count per table ---"); | ||||||
|  |             Map<String, Integer> records = TestCaseUtils.recordsInAllTables(daoHelper); | ||||||
|  |             records.keySet().stream().filter(s -> s.equals("MEDIA_FILE") // 20
 | ||||||
|  |                     | s.equals("ARTIST") // 5
 | ||||||
|  |                     | s.equals("MUSIC_FOLDER")// 3
 | ||||||
|  |                     | s.equals("ALBUM"))// 5
 | ||||||
|  |                     .forEach(tableName -> System.out | ||||||
|  |                             .println("\t" + tableName + " : " + records.get(tableName).toString())); | ||||||
|  |             // Music Folder Music must have 3 children
 | ||||||
|  |             List<MediaFile> listeMusicChildren = mediaFileDao.getChildrenOf( | ||||||
|  |                     new File(MusicFolderTestData.resolveMusicFolderPath()).getPath()); | ||||||
|  |             Assert.assertEquals(3, listeMusicChildren.size()); | ||||||
|  |             // Music Folder Music2 must have 1 children
 | ||||||
|  |             List<MediaFile> listeMusic2Children = mediaFileDao.getChildrenOf( | ||||||
|  |                     new File(MusicFolderTestData.resolveMusic2FolderPath()).getPath()); | ||||||
|  |             Assert.assertEquals(1, listeMusic2Children.size()); | ||||||
|  |             System.out.println("--- *********************** ---"); | ||||||
|  |             dataBasePopulated = true; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |   @Test | ||||||
|  |   public void testSearchTypical() { | ||||||
|  | 
 | ||||||
|  |     /* | ||||||
|  |      * A simple test that is expected to easily detect API syntax differences when updating lucene. | ||||||
|  |      * Complete route coverage and data coverage in this case alone are not conscious. | ||||||
|  |      */ | ||||||
|  | 
 | ||||||
|  |     List<MusicFolder> allMusicFolders = musicFolderDao.getAllMusicFolders(); | ||||||
|  |     Assert.assertEquals(3, allMusicFolders.size()); | ||||||
|  | 
 | ||||||
|  |     // *** testSearch() ***
 | ||||||
|  | 
 | ||||||
|  |     String query = "Sarah Walker"; | ||||||
|  |     final SearchCriteria searchCriteria = new SearchCriteria(); | ||||||
|  |     searchCriteria.setQuery(query); | ||||||
|  |     searchCriteria.setCount(Integer.MAX_VALUE); | ||||||
|  |     searchCriteria.setOffset(0); | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          * _ID3_ALBUMARTIST_ Sarah Walker/Nash Ensemble | ||||||
|  |          * Should find any version of Lucene. | ||||||
|  |          */ | ||||||
|  |         SearchResult result = searchService.search(searchCriteria, allMusicFolders, | ||||||
|  |                 IndexType.ALBUM); | ||||||
|  |         Assert.assertEquals("(0) Specify '" + query + "' as query, total Hits is", 1, | ||||||
|  |                 result.getTotalHits()); | ||||||
|  |         Assert.assertEquals("(1) Specify artist '" + query + "' as query. Artist SIZE is", 0, | ||||||
|  |                 result.getArtists().size()); | ||||||
|  |         Assert.assertEquals("(2) Specify artist '" + query + "' as query. Album SIZE is", 0, | ||||||
|  |                 result.getAlbums().size()); | ||||||
|  |         Assert.assertEquals("(3) Specify artist '" + query + "' as query, MediaFile SIZE is", 1, | ||||||
|  |                 result.getMediaFiles().size()); | ||||||
|  |         Assert.assertEquals("(4) ", MediaType.ALBUM, result.getMediaFiles().get(0).getMediaType()); | ||||||
|  |         Assert.assertEquals( | ||||||
|  |                 "(5) Specify artist '" + query + "' as query, and get a album. Name is ", | ||||||
|  |                 "_ID3_ALBUMARTIST_ Sarah Walker/Nash Ensemble", | ||||||
|  |                 result.getMediaFiles().get(0).getArtist()); | ||||||
|  |         Assert.assertEquals( | ||||||
|  |                 "(6) Specify artist '" + query + "' as query, and get a album. Name is ", | ||||||
|  |                 "_ID3_ALBUM_ Ravel - Chamber Music With Voice", | ||||||
|  |                 result.getMediaFiles().get(0).getAlbumName()); | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          * _ID3_ALBUM_ Ravel - Chamber Music With Voice | ||||||
|  |          * Should find any version of Lucene. | ||||||
|  |          */ | ||||||
|  |         query = "music"; | ||||||
|  |         searchCriteria.setQuery(query); | ||||||
|  |         result = searchService.search(searchCriteria, allMusicFolders, IndexType.ALBUM_ID3); | ||||||
|  |         Assert.assertEquals("Specify '" + query + "' as query, total Hits is", 1, | ||||||
|  |                 result.getTotalHits()); | ||||||
|  |         Assert.assertEquals("(7) Specify '" + query + "' as query, and get a song. Artist SIZE is ", | ||||||
|  |                 0, result.getArtists().size()); | ||||||
|  |         Assert.assertEquals("(8) Specify '" + query + "' as query, and get a song. Album SIZE is ", | ||||||
|  |                 1, result.getAlbums().size()); | ||||||
|  |         Assert.assertEquals( | ||||||
|  |                 "(9) Specify '" + query + "' as query, and get a song. MediaFile SIZE is ", 0, | ||||||
|  |                 result.getMediaFiles().size()); | ||||||
|  |         Assert.assertEquals("(9) Specify '" + query + "' as query, and get a album. Name is ", | ||||||
|  |                 "_ID3_ALBUMARTIST_ Sarah Walker/Nash Ensemble", | ||||||
|  |                 result.getAlbums().get(0).getArtist()); | ||||||
|  |         Assert.assertEquals("(10) Specify '" + query + "' as query, and get a album. Name is ", | ||||||
|  |                 "_ID3_ALBUM_ Ravel - Chamber Music With Voice", | ||||||
|  |                 result.getAlbums().get(0).getName()); | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          * _ID3_ALBUM_ Ravel - Chamber Music With Voice | ||||||
|  |          * Should find any version of Lucene. | ||||||
|  |          */ | ||||||
|  |         query = "Ravel - Chamber Music"; | ||||||
|  |         searchCriteria.setQuery(query); | ||||||
|  |         result = searchService.search(searchCriteria, allMusicFolders, IndexType.SONG); | ||||||
|  |         Assert.assertEquals("(11) Specify album '" + query + "' as query, total Hits is", 2, | ||||||
|  |                 result.getTotalHits()); | ||||||
|  |         Assert.assertEquals("(12) Specify album '" + query + "', and get a song. Artist SIZE is", 0, | ||||||
|  |                 result.getArtists().size()); | ||||||
|  |         Assert.assertEquals("(13) Specify album '" + query + "', and get a song. Album SIZE is", 0, | ||||||
|  |                 result.getAlbums().size()); | ||||||
|  |         Assert.assertEquals("(14) Specify album '" + query + "', and get a song. MediaFile SIZE is", | ||||||
|  |                 2, result.getMediaFiles().size()); | ||||||
|  |         Assert.assertEquals("(15) Specify album '" + query + "', and get songs. The first song is ", | ||||||
|  |                 "01 - Gaspard de la Nuit - i. Ondine", result.getMediaFiles().get(0).getTitle()); | ||||||
|  |         Assert.assertEquals( | ||||||
|  |                 "(16) Specify album '" + query + "', and get songs. The second song is ", | ||||||
|  |                 "02 - Gaspard de la Nuit - ii. Le Gibet", result.getMediaFiles().get(1).getTitle()); | ||||||
|  | 
 | ||||||
|  |         // *** testSearchByName() ***
 | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          * _ID3_ALBUM_ Sackcloth 'n' Ashes | ||||||
|  |          * Should be 1 in Lucene 3.0(Because Single quate is not a delimiter). | ||||||
|  |          */ | ||||||
|  |         query = "Sackcloth 'n' Ashes"; | ||||||
|  |         ParamSearchResult<Album> albumResult = searchService.searchByName(query, 0, | ||||||
|  |                 Integer.MAX_VALUE, allMusicFolders, Album.class); | ||||||
|  |         Assert.assertEquals( | ||||||
|  |                 "(17) Specify album name '" + query + "' as the name, and get an album.", 1, | ||||||
|  |                 albumResult.getItems().size()); | ||||||
|  |         Assert.assertEquals("(18) Specify '" + query + "' as the name, The album name is ", | ||||||
|  |                 "_ID3_ALBUM_ Sackcloth 'n' Ashes", albumResult.getItems().get(0).getName()); | ||||||
|  |         Assert.assertEquals( | ||||||
|  |                 "(19) Whether the acquired album contains data of the specified album name", 1L, | ||||||
|  |                 albumResult.getItems().stream() | ||||||
|  |                         .filter(r -> "_ID3_ALBUM_ Sackcloth \'n\' Ashes".equals(r.getName())) | ||||||
|  |                         .count()); | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          * Should be 0 in Lucene 3.0(Since the slash is not a delimiter). | ||||||
|  |          */ | ||||||
|  |         query = "lker/Nash"; | ||||||
|  |         ParamSearchResult<ArtistID3> artistId3Result = searchService.searchByName(query, 0, | ||||||
|  |                 Integer.MAX_VALUE, allMusicFolders, ArtistID3.class); | ||||||
|  |         Assert.assertEquals("(20) Specify '" + query + "' as the name, and get an artist.", 0, | ||||||
|  |                 artistId3Result.getItems().size()); | ||||||
|  |         ParamSearchResult<Artist> artistResult = searchService.searchByName(query, 0, | ||||||
|  |                 Integer.MAX_VALUE, allMusicFolders, Artist.class); | ||||||
|  |         Assert.assertEquals("(21) Specify '" + query + "' as the name, and get an artist.", 0, | ||||||
|  |                 artistResult.getItems().size()); | ||||||
|  | 
 | ||||||
|  |         // *** testGetRandomSongs() ***
 | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          * Regardless of the Lucene version, | ||||||
|  |          * RandomSearchCriteria can specify null and means the maximum range. | ||||||
|  |          * 11 should be obtainable. | ||||||
|  |          */ | ||||||
|  |         RandomSearchCriteria randomSearchCriteria = new RandomSearchCriteria(Integer.MAX_VALUE, // count
 | ||||||
|  |                 null, // genre,
 | ||||||
|  |                 null, // fromYear
 | ||||||
|  |                 null, // toYear
 | ||||||
|  |                 allMusicFolders // musicFolders
 | ||||||
|  |         ); | ||||||
|  |         List<MediaFile> allRandomSongs = searchService.getRandomSongs(randomSearchCriteria); | ||||||
|  |         Assert.assertEquals( | ||||||
|  |                 "(22) Specify MAX_VALUE as the upper limit, and randomly acquire songs.", 11, | ||||||
|  |                 allRandomSongs.size()); | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          * Regardless of the Lucene version, | ||||||
|  |          * 7 should be obtainable. | ||||||
|  |          */ | ||||||
|  |         randomSearchCriteria = new RandomSearchCriteria(Integer.MAX_VALUE, // count
 | ||||||
|  |                 null, // genre,
 | ||||||
|  |                 1900, // fromYear
 | ||||||
|  |                 null, // toYear
 | ||||||
|  |                 allMusicFolders // musicFolders
 | ||||||
|  |         ); | ||||||
|  |         allRandomSongs = searchService.getRandomSongs(randomSearchCriteria); | ||||||
|  |         Assert.assertEquals("(23) Specify 1900 as 'fromYear', and randomly acquire songs.", 7, | ||||||
|  |                 allRandomSongs.size()); | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          * Regardless of the Lucene version, | ||||||
|  |          * It should be 0 because it is a non-existent genre. | ||||||
|  |          */ | ||||||
|  |         randomSearchCriteria = new RandomSearchCriteria(Integer.MAX_VALUE, // count
 | ||||||
|  |                 "Chamber Music", // genre,
 | ||||||
|  |                 null, // fromYear
 | ||||||
|  |                 null, // toYear
 | ||||||
|  |                 allMusicFolders // musicFolders
 | ||||||
|  |         ); | ||||||
|  |         allRandomSongs = searchService.getRandomSongs(randomSearchCriteria); | ||||||
|  |         Assert.assertEquals("(24) Specify music as 'genre', and randomly acquire songs.", 0, | ||||||
|  |                 allRandomSongs.size()); | ||||||
|  | 
 | ||||||
|  |         // *** testGetRandomAlbums() ***
 | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          * Acquisition of maximum number(5). | ||||||
|  |          */ | ||||||
|  |         List<Album> allAlbums = albumDao.getAlphabeticalAlbums(0, 0, true, true, allMusicFolders); | ||||||
|  |         Assert.assertEquals("(25) Get all albums with Dao.", 5, allAlbums.size()); | ||||||
|  |         List<MediaFile> allRandomAlbums = searchService.getRandomAlbums(Integer.MAX_VALUE, | ||||||
|  |                 allMusicFolders); | ||||||
|  |         Assert.assertEquals("(26) Specify Integer.MAX_VALUE as the upper limit," | ||||||
|  |                 + "and randomly acquire albums(file struct).", 5, allRandomAlbums.size()); | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          * Acquisition of maximum number(5). | ||||||
|  |          */ | ||||||
|  |         List<Album> allRandomAlbumsId3 = searchService.getRandomAlbumsId3(Integer.MAX_VALUE, | ||||||
|  |                 allMusicFolders); | ||||||
|  |         Assert.assertEquals( | ||||||
|  |                 "(27) Specify Integer.MAX_VALUE as the upper limit, and randomly acquire albums(ID3).", | ||||||
|  |                 5, allRandomAlbumsId3.size()); | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          * Total is 4. | ||||||
|  |          */ | ||||||
|  |         query = "ID 3 ARTIST"; | ||||||
|  |         searchCriteria.setQuery(query); | ||||||
|  |         result = searchService.search(searchCriteria, allMusicFolders, IndexType.ARTIST_ID3); | ||||||
|  |         Assert.assertEquals("(28) Specify '" + query + "', total Hits is", 4, | ||||||
|  |                 result.getTotalHits()); | ||||||
|  |         Assert.assertEquals("(29) Specify '" + query + "', and get an artists. Artist SIZE is ", 4, | ||||||
|  |                 result.getArtists().size()); | ||||||
|  |         Assert.assertEquals("(30) Specify '" + query + "', and get a artists. Album SIZE is ", 0, | ||||||
|  |                 result.getAlbums().size()); | ||||||
|  |         Assert.assertEquals("(31) Specify '" + query + "', and get a artists. MediaFile SIZE is ", | ||||||
|  |                 0, result.getMediaFiles().size()); | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          * Three hits to the artist. | ||||||
|  |          * ALBUMARTIST is not registered with these. | ||||||
|  |          * Therefore, the registered value of ARTIST is substituted in ALBUMARTIST. | ||||||
|  |          */ | ||||||
|  |         long count = result.getArtists().stream() | ||||||
|  |                 .filter(a -> a.getName().startsWith("_ID3_ARTIST_")).count(); | ||||||
|  |         Assert.assertEquals("(32) Artist whose name contains \\\"_ID3_ARTIST_\\\" is 3 records.", | ||||||
|  |                 3L, count); | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          * The structure of "01 - Sonata Violin & Cello I. Allegro.ogg" | ||||||
|  |          * ARTIST -> _ID3_ARTIST_ Sarah Walker/Nash Ensemble | ||||||
|  |          * ALBUMARTIST -> _ID3_ALBUMARTIST_ Sarah Walker/Nash Ensemble | ||||||
|  |          * (The result must not contain duplicates. And ALBUMARTIST must be returned correctly.) | ||||||
|  |          */ | ||||||
|  |         count = result.getArtists().stream() | ||||||
|  |                 .filter(a -> a.getName().startsWith("_ID3_ALBUMARTIST_")).count(); | ||||||
|  |         Assert.assertEquals("(33) Artist whose name is \"_ID3_ARTIST_\" is 1 records.", 1L, count); | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          * Below is a simple loop test. | ||||||
|  |          * How long is the total time? | ||||||
|  |          */ | ||||||
|  |         int countForEachMethod = 500; | ||||||
|  |         String[] randomWords4Search = createRandomWords(countForEachMethod); | ||||||
|  |         String[] randomWords4SearchByName = createRandomWords(countForEachMethod); | ||||||
|  | 
 | ||||||
|  |         Timer globalTimer = metrics | ||||||
|  |                 .timer(MetricRegistry.name(SearchServiceTestCase.class, "Timer.global")); | ||||||
|  |         final Timer.Context globalTimerContext = globalTimer.time(); | ||||||
|  | 
 | ||||||
|  |         System.out.println("--- Random search (" + countForEachMethod * 5 + " times) ---"); | ||||||
|  | 
 | ||||||
|  |         // testSearch()
 | ||||||
|  |         Arrays.stream(randomWords4Search).forEach(w -> { | ||||||
|  |             searchCriteria.setQuery(w); | ||||||
|  |             searchService.search(searchCriteria, allMusicFolders, IndexType.ALBUM); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         // testSearchByName()
 | ||||||
|  |         Arrays.stream(randomWords4SearchByName).forEach(w -> { | ||||||
|  |             searchService.searchByName(w, 0, Integer.MAX_VALUE, allMusicFolders, Artist.class); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         // testGetRandomSongs()
 | ||||||
|  |         RandomSearchCriteria criteria = new RandomSearchCriteria(Integer.MAX_VALUE, // count
 | ||||||
|  |                 null, // genre,
 | ||||||
|  |                 null, // fromYear
 | ||||||
|  |                 null, // toYear
 | ||||||
|  |                 allMusicFolders // musicFolders
 | ||||||
|  |         ); | ||||||
|  |         for (int i = 0; i < countForEachMethod; i++) { | ||||||
|  |             searchService.getRandomSongs(criteria); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // testGetRandomAlbums()
 | ||||||
|  |         for (int i = 0; i < countForEachMethod; i++) { | ||||||
|  |             searchService.getRandomAlbums(Integer.MAX_VALUE, allMusicFolders); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // testGetRandomAlbumsId3()
 | ||||||
|  |         for (int i = 0; i < countForEachMethod; i++) { | ||||||
|  |             searchService.getRandomAlbumsId3(Integer.MAX_VALUE, allMusicFolders); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         globalTimerContext.stop(); | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          * Whether or not IndexReader is exhausted. | ||||||
|  |          */ | ||||||
|  |         query = "Sarah Walker"; | ||||||
|  |         searchCriteria.setQuery(query); | ||||||
|  |         result = searchService.search(searchCriteria, allMusicFolders, IndexType.ALBUM); | ||||||
|  |         Assert.assertEquals("(35) Can the normal case be implemented.", 0, | ||||||
|  |                 result.getArtists().size()); | ||||||
|  |         Assert.assertEquals("(36) Can the normal case be implemented.", 0, | ||||||
|  |                 result.getAlbums().size()); | ||||||
|  |         Assert.assertEquals("(37) Can the normal case be implemented.", 1, | ||||||
|  |                 result.getMediaFiles().size()); | ||||||
|  |         Assert.assertEquals("(38) Can the normal case be implemented.", MediaType.ALBUM, | ||||||
|  |                 result.getMediaFiles().get(0).getMediaType()); | ||||||
|  |         Assert.assertEquals("(39) Can the normal case be implemented.", | ||||||
|  |                 "_ID3_ALBUMARTIST_ Sarah Walker/Nash Ensemble", | ||||||
|  |                 result.getMediaFiles().get(0).getArtist()); | ||||||
|  | 
 | ||||||
|  |         System.out.println("--- SUCCESS ---"); | ||||||
|  | 
 | ||||||
|  |         ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics) | ||||||
|  |                 .convertRatesTo(TimeUnit.SECONDS).convertDurationsTo(TimeUnit.MILLISECONDS).build(); | ||||||
|  |         reporter.report(); | ||||||
|  | 
 | ||||||
|  |         System.out.println("End. "); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static String[] createRandomWords(int count) { | ||||||
|  |         String[] randomStrings = new String[count]; | ||||||
|  |         Random random = new Random(); | ||||||
|  |         for (int i = 0; i < count; i++) { | ||||||
|  |             char[] word = new char[random.nextInt(8) + 3]; | ||||||
|  |             for (int j = 0; j < word.length; j++) { | ||||||
|  |                 word[j] = (char) ('a' + random.nextInt(26)); | ||||||
|  |             } | ||||||
|  |             randomStrings[i] = new String(word); | ||||||
|  |         } | ||||||
|  |         return randomStrings; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
					Loading…
					
					
				
		Reference in new issue