parent
							
								
									42bced139f
								
							
						
					
					
						commit
						767b39ed5b
					
				| @ -1,666 +1,86 @@ | ||||
| /* | ||||
|  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; | ||||
| 
 | ||||
| import com.google.common.collect.Lists; | ||||
| import org.airsonic.player.dao.AlbumDao; | ||||
| import org.airsonic.player.dao.ArtistDao; | ||||
| import org.airsonic.player.domain.*; | ||||
| import org.airsonic.player.util.FileUtil; | ||||
| import org.apache.lucene.analysis.*; | ||||
| 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.analysis.tokenattributes.TermAttribute; | ||||
| import org.apache.lucene.document.Document; | ||||
| import org.apache.lucene.document.Field; | ||||
| import org.apache.lucene.document.NumericField; | ||||
| import org.apache.lucene.index.IndexReader; | ||||
| import org.apache.lucene.index.IndexWriter; | ||||
| import org.apache.lucene.index.Term; | ||||
| import org.apache.lucene.queryParser.MultiFieldQueryParser; | ||||
| import org.apache.lucene.queryParser.QueryParser; | ||||
| import org.apache.lucene.search.*; | ||||
| 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.store.Directory; | ||||
| import org.apache.lucene.store.FSDirectory; | ||||
| import org.apache.lucene.util.NumericUtils; | ||||
| import org.apache.lucene.util.Version; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.stereotype.Service; | ||||
| 
 | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.io.Reader; | ||||
| import java.io.StringReader; | ||||
| import java.util.*; | ||||
| 
 | ||||
| import static org.airsonic.player.service.SearchService.IndexType.*; | ||||
| 
 | ||||
| /** | ||||
|  * Performs Lucene-based searching and indexing. | ||||
|  * | ||||
|  * @author Sindre Mehus | ||||
|  * @version $Id$ | ||||
|  * @see MediaScannerService | ||||
|  */ | ||||
| @Service | ||||
| public class SearchService { | ||||
| 
 | ||||
|     private static final Logger LOG = LoggerFactory.getLogger(SearchService.class); | ||||
| 
 | ||||
|     private static final String FIELD_ID = "id"; | ||||
|     private static final String FIELD_TITLE = "title"; | ||||
|     private static final String FIELD_ALBUM = "album"; | ||||
|     private static final String FIELD_ARTIST = "artist"; | ||||
|     private static final String FIELD_GENRE = "genre"; | ||||
|     private static final String FIELD_YEAR = "year"; | ||||
|     private static final String FIELD_MEDIA_TYPE = "mediaType"; | ||||
|     private static final String FIELD_FOLDER = "folder"; | ||||
|     private static final String FIELD_FOLDER_ID = "folderId"; | ||||
| 
 | ||||
|     private static final Version LUCENE_VERSION = Version.LUCENE_30; | ||||
|     private static final String LUCENE_DIR = "lucene2"; | ||||
| 
 | ||||
|     @Autowired | ||||
|     private MediaFileService mediaFileService; | ||||
|     @Autowired | ||||
|     private ArtistDao artistDao; | ||||
|     @Autowired | ||||
|     private AlbumDao albumDao; | ||||
| 
 | ||||
|     private IndexWriter artistWriter; | ||||
|     private IndexWriter artistId3Writer; | ||||
|     private IndexWriter albumWriter; | ||||
|     private IndexWriter albumId3Writer; | ||||
|     private IndexWriter songWriter; | ||||
| 
 | ||||
|     public SearchService() { | ||||
|         removeLocks(); | ||||
|     } | ||||
| 
 | ||||
|     public 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 index(MediaFile mediaFile) { | ||||
|         try { | ||||
|             if (mediaFile.isFile()) { | ||||
|                 songWriter.addDocument(SONG.createDocument(mediaFile)); | ||||
|             } else if (mediaFile.isAlbum()) { | ||||
|                 albumWriter.addDocument(ALBUM.createDocument(mediaFile)); | ||||
|             } else { | ||||
|                 artistWriter.addDocument(ARTIST.createDocument(mediaFile)); | ||||
|             } | ||||
|         } catch (Exception x) { | ||||
|             LOG.error("Failed to create search index for " + mediaFile, x); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void index(Artist artist, MusicFolder musicFolder) { | ||||
|         try { | ||||
|             artistId3Writer.addDocument(ARTIST_ID3.createDocument(artist, musicFolder)); | ||||
|         } catch (Exception x) { | ||||
|             LOG.error("Failed to create search index for " + artist, x); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void index(Album album) { | ||||
|         try { | ||||
|             albumId3Writer.addDocument(ALBUM_ID3.createDocument(album)); | ||||
|         } catch (Exception x) { | ||||
|             LOG.error("Failed to create search index for " + album, 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); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     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 = createIndexReader(indexType); | ||||
|             Searcher searcher = new IndexSearcher(reader); | ||||
|             Analyzer analyzer = new CustomAnalyzer(); | ||||
| 
 | ||||
|             MultiFieldQueryParser queryParser = new MultiFieldQueryParser(LUCENE_VERSION, indexType.getFields(), analyzer, 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(FIELD_FOLDER_ID, NumericUtils.intToPrefixCoded(musicFolder.getId())))); | ||||
|                 } else { | ||||
|                     musicFolderQueries.add(new SpanTermQuery(new Term(FIELD_FOLDER, musicFolder.getPath().getPath()))); | ||||
|                 } | ||||
|             } | ||||
|             query.add(new SpanOrQuery(musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])), BooleanClause.Occur.MUST); | ||||
| 
 | ||||
|             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++) { | ||||
|                 Document doc = searcher.doc(topDocs.scoreDocs[i].doc); | ||||
|                 switch (indexType) { | ||||
|                     case SONG: | ||||
|                     case ARTIST: | ||||
|                     case ALBUM: | ||||
|                         MediaFile mediaFile = mediaFileService.getMediaFile(Integer.valueOf(doc.get(FIELD_ID))); | ||||
|                         addIfNotNull(mediaFile, result.getMediaFiles()); | ||||
|                         break; | ||||
|                     case ARTIST_ID3: | ||||
|                         Artist artist = artistDao.getArtist(Integer.valueOf(doc.get(FIELD_ID))); | ||||
|                         addIfNotNull(artist, result.getArtists()); | ||||
|                         break; | ||||
|                     case ALBUM_ID3: | ||||
|                         Album album = albumDao.getAlbum(Integer.valueOf(doc.get(FIELD_ID))); | ||||
|                         addIfNotNull(album, result.getAlbums()); | ||||
|                         break; | ||||
|                     default: | ||||
|                         break; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|         } catch (Throwable x) { | ||||
|             LOG.error("Failed to execute Lucene search.", x); | ||||
|         } finally { | ||||
|             FileUtil.closeQuietly(reader); | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     private String analyzeQuery(String query) throws IOException { | ||||
|         StringBuilder result = new StringBuilder(); | ||||
|         ASCIIFoldingFilter filter = new ASCIIFoldingFilter(new StandardTokenizer(LUCENE_VERSION, new StringReader(query))); | ||||
|         TermAttribute termAttribute = filter.getAttribute(TermAttribute.class); | ||||
|         while (filter.incrementToken()) { | ||||
|             result.append(termAttribute.term()).append("* "); | ||||
|         } | ||||
|         return result.toString(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns a number of random songs. | ||||
|      * | ||||
|      * @param criteria Search criteria. | ||||
|      * @return List of random songs. | ||||
|      */ | ||||
|     public List<MediaFile> getRandomSongs(RandomSearchCriteria criteria) { | ||||
|         List<MediaFile> result = new ArrayList<MediaFile>(); | ||||
| 
 | ||||
|         IndexReader reader = null; | ||||
|         try { | ||||
|             reader = createIndexReader(SONG); | ||||
|             Searcher searcher = new IndexSearcher(reader); | ||||
| 
 | ||||
|             BooleanQuery query = new BooleanQuery(); | ||||
|             query.add(new TermQuery(new Term(FIELD_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(FIELD_GENRE, genre)), BooleanClause.Occur.MUST); | ||||
|             } | ||||
|             if (criteria.getFromYear() != null || criteria.getToYear() != null) { | ||||
|                 NumericRangeQuery<Integer> rangeQuery = NumericRangeQuery.newIntRange(FIELD_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(FIELD_FOLDER, musicFolder.getPath().getPath()))); | ||||
|             } | ||||
|             query.add(new SpanOrQuery(musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])), BooleanClause.Occur.MUST); | ||||
| 
 | ||||
|             TopDocs topDocs = searcher.search(query, null, Integer.MAX_VALUE); | ||||
|             List<ScoreDoc> scoreDocs = Lists.newArrayList(topDocs.scoreDocs); | ||||
|             Random random = new Random(System.currentTimeMillis()); | ||||
| 
 | ||||
|             while (!scoreDocs.isEmpty() && result.size() < criteria.getCount()) { | ||||
|                 int index = random.nextInt(scoreDocs.size()); | ||||
|                 Document doc = searcher.doc(scoreDocs.remove(index).doc); | ||||
|                 int id = Integer.valueOf(doc.get(FIELD_ID)); | ||||
|                 try { | ||||
|                     addIfNotNull(mediaFileService.getMediaFile(id), result); | ||||
|                 } catch (Exception x) { | ||||
|                     LOG.warn("Failed to get media file " + id); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|         } catch (Throwable x) { | ||||
|             LOG.error("Failed to search or random songs.", x); | ||||
|         } finally { | ||||
|             FileUtil.closeQuietly(reader); | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     private static String normalizeGenre(String genre) { | ||||
|         return genre.toLowerCase().replace(" ", "").replace("-", ""); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns a number of random albums. | ||||
|      * | ||||
|      * @param count        Number of albums to return. | ||||
|      * @param musicFolders Only return albums from these folders. | ||||
|      * @return List of random albums. | ||||
|      */ | ||||
|     public List<MediaFile> getRandomAlbums(int count, List<MusicFolder> musicFolders) { | ||||
|         List<MediaFile> result = new ArrayList<MediaFile>(); | ||||
| 
 | ||||
|         IndexReader reader = null; | ||||
|         try { | ||||
|             reader = createIndexReader(ALBUM); | ||||
|             Searcher searcher = new IndexSearcher(reader); | ||||
| 
 | ||||
|             List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>(); | ||||
|             for (MusicFolder musicFolder : musicFolders) { | ||||
|                 musicFolderQueries.add(new SpanTermQuery(new Term(FIELD_FOLDER, musicFolder.getPath().getPath()))); | ||||
|             } | ||||
|             Query query = new SpanOrQuery(musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])); | ||||
| 
 | ||||
|             TopDocs topDocs = searcher.search(query, null, Integer.MAX_VALUE); | ||||
|             List<ScoreDoc> scoreDocs = Lists.newArrayList(topDocs.scoreDocs); | ||||
|             Random random = new Random(System.currentTimeMillis()); | ||||
| 
 | ||||
|             while (!scoreDocs.isEmpty() && result.size() < count) { | ||||
|                 int index = random.nextInt(scoreDocs.size()); | ||||
|                 Document doc = searcher.doc(scoreDocs.remove(index).doc); | ||||
|                 int id = Integer.valueOf(doc.get(FIELD_ID)); | ||||
|                 try { | ||||
|                     addIfNotNull(mediaFileService.getMediaFile(id), result); | ||||
|                 } catch (Exception x) { | ||||
|                     LOG.warn("Failed to get media file " + id, x); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|         } catch (Throwable x) { | ||||
|             LOG.error("Failed to search for random albums.", x); | ||||
|         } finally { | ||||
|             FileUtil.closeQuietly(reader); | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns a number of random albums, using ID3 tag. | ||||
|      * | ||||
|      * @param count        Number of albums to return. | ||||
|      * @param musicFolders Only return albums from these folders. | ||||
|      * @return List of random albums. | ||||
|      */ | ||||
|     public List<Album> getRandomAlbumsId3(int count, List<MusicFolder> musicFolders) { | ||||
|         List<Album> result = new ArrayList<Album>(); | ||||
| 
 | ||||
|         IndexReader reader = null; | ||||
|         try { | ||||
|             reader = createIndexReader(ALBUM_ID3); | ||||
|             Searcher searcher = new IndexSearcher(reader); | ||||
| 
 | ||||
|             List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>(); | ||||
|             for (MusicFolder musicFolder : musicFolders) { | ||||
|                 musicFolderQueries.add(new SpanTermQuery(new Term(FIELD_FOLDER_ID, NumericUtils.intToPrefixCoded(musicFolder.getId())))); | ||||
|             } | ||||
|             Query query = new SpanOrQuery(musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])); | ||||
|             TopDocs topDocs = searcher.search(query, null, Integer.MAX_VALUE); | ||||
|             List<ScoreDoc> scoreDocs = Lists.newArrayList(topDocs.scoreDocs); | ||||
|             Random random = new Random(System.currentTimeMillis()); | ||||
| 
 | ||||
|             while (!scoreDocs.isEmpty() && result.size() < count) { | ||||
|                 int index = random.nextInt(scoreDocs.size()); | ||||
|                 Document doc = searcher.doc(scoreDocs.remove(index).doc); | ||||
|                 int id = Integer.valueOf(doc.get(FIELD_ID)); | ||||
|                 try { | ||||
|                     addIfNotNull(albumDao.getAlbum(id), result); | ||||
|                 } catch (Exception x) { | ||||
|                     LOG.warn("Failed to get album file " + id, x); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|         } catch (Throwable x) { | ||||
|             LOG.error("Failed to search for random albums.", x); | ||||
|         } finally { | ||||
|             FileUtil.closeQuietly(reader); | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     public <T> ParamSearchResult<T> searchByName(String name, int offset, int count, List<MusicFolder> folderList, Class<T> clazz) { | ||||
|         IndexType indexType = null; | ||||
|         String field = null; | ||||
|         if (clazz.isAssignableFrom(Album.class)) { | ||||
|             indexType = IndexType.ALBUM_ID3; | ||||
|             field = FIELD_ALBUM; | ||||
|         } else if (clazz.isAssignableFrom(Artist.class)) { | ||||
|             indexType = IndexType.ARTIST_ID3; | ||||
|             field = FIELD_ARTIST; | ||||
|         } else if (clazz.isAssignableFrom(MediaFile.class)) { | ||||
|             indexType = IndexType.SONG; | ||||
|             field = FIELD_TITLE; | ||||
|         } | ||||
|         ParamSearchResult<T> result = new ParamSearchResult<T>(); | ||||
|         // we only support album, artist, and song for now
 | ||||
|         if (indexType == null || field == null) { | ||||
|             return result; | ||||
|         } | ||||
| 
 | ||||
|         result.setOffset(offset); | ||||
| 
 | ||||
|         IndexReader reader = null; | ||||
| 
 | ||||
|         try { | ||||
|             reader = createIndexReader(indexType); | ||||
|             Searcher searcher = new IndexSearcher(reader); | ||||
|             Analyzer analyzer = new CustomAnalyzer(); | ||||
|             QueryParser queryParser = new QueryParser(LUCENE_VERSION, field, analyzer); | ||||
| 
 | ||||
|             Query q = queryParser.parse(name + "*"); | ||||
| 
 | ||||
|             Sort sort = new Sort(new SortField(field, SortField.STRING)); | ||||
|             TopDocs topDocs = searcher.search(q, 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); | ||||
|                 switch (indexType) { | ||||
|                 case SONG: | ||||
|                     MediaFile mediaFile = mediaFileService.getMediaFile(Integer.valueOf(doc.get(FIELD_ID))); | ||||
|                     addIfNotNull(clazz.cast(mediaFile), result.getItems()); | ||||
|                     break; | ||||
|                 case ARTIST_ID3: | ||||
|                     Artist artist = artistDao.getArtist(Integer.valueOf(doc.get(FIELD_ID))); | ||||
|                     addIfNotNull(clazz.cast(artist), result.getItems()); | ||||
|                     break; | ||||
|                 case ALBUM_ID3: | ||||
|                     Album album = albumDao.getAlbum(Integer.valueOf(doc.get(FIELD_ID))); | ||||
|                     addIfNotNull(clazz.cast(album), result.getItems()); | ||||
|                     break; | ||||
|                 default: | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         } catch (Throwable x) { | ||||
|             LOG.error("Failed to execute Lucene search.", x); | ||||
|         } finally { | ||||
|             FileUtil.closeQuietly(reader); | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     private <T> void addIfNotNull(T value, List<T> list) { | ||||
|         if (value != null) { | ||||
|             list.add(value); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private IndexWriter createIndexWriter(IndexType indexType) throws IOException { | ||||
|         File dir = getIndexDirectory(indexType); | ||||
|         return new IndexWriter(FSDirectory.open(dir), new CustomAnalyzer(), true, new IndexWriter.MaxFieldLength(10)); | ||||
|     } | ||||
| 
 | ||||
|     private IndexReader createIndexReader(IndexType indexType) throws IOException { | ||||
|         File dir = getIndexDirectory(indexType); | ||||
|         return IndexReader.open(FSDirectory.open(dir), true); | ||||
|     } | ||||
| 
 | ||||
|     private File getIndexRootDirectory() { | ||||
|         return new File(SettingsService.getAirsonicHome(), LUCENE_DIR); | ||||
|     } | ||||
| 
 | ||||
|     private File getIndexDirectory(IndexType indexType) { | ||||
|         return new File(getIndexRootDirectory(), indexType.toString().toLowerCase()); | ||||
|     } | ||||
| 
 | ||||
|     private void removeLocks() { | ||||
|         for (IndexType indexType : IndexType.values()) { | ||||
|             Directory dir = null; | ||||
|             try { | ||||
|                 dir = FSDirectory.open(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); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void setMediaFileService(MediaFileService mediaFileService) { | ||||
|         this.mediaFileService = mediaFileService; | ||||
|     } | ||||
| 
 | ||||
|     public void setArtistDao(ArtistDao artistDao) { | ||||
|         this.artistDao = artistDao; | ||||
|     } | ||||
| 
 | ||||
|     public void setAlbumDao(AlbumDao albumDao) { | ||||
|         this.albumDao = albumDao; | ||||
|     } | ||||
| 
 | ||||
|     public static enum IndexType { | ||||
| 
 | ||||
|         SONG(new String[]{FIELD_TITLE, FIELD_ARTIST}, FIELD_TITLE) { | ||||
|             @Override | ||||
|             public Document createDocument(MediaFile mediaFile) { | ||||
|                 Document doc = new Document(); | ||||
|                 doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(mediaFile.getId())); | ||||
|                 doc.add(new Field(FIELD_MEDIA_TYPE, mediaFile.getMediaType().name(), Field.Store.NO, Field.Index.ANALYZED_NO_NORMS)); | ||||
| 
 | ||||
|                 if (mediaFile.getTitle() != null) { | ||||
|                     doc.add(new Field(FIELD_TITLE, mediaFile.getTitle(), Field.Store.YES, Field.Index.ANALYZED)); | ||||
|                 } | ||||
|                 if (mediaFile.getArtist() != null) { | ||||
|                     doc.add(new Field(FIELD_ARTIST, mediaFile.getArtist(), Field.Store.YES, Field.Index.ANALYZED)); | ||||
|                 } | ||||
|                 if (mediaFile.getGenre() != null) { | ||||
|                     doc.add(new Field(FIELD_GENRE, normalizeGenre(mediaFile.getGenre()), Field.Store.NO, Field.Index.ANALYZED)); | ||||
|                 } | ||||
|                 if (mediaFile.getYear() != null) { | ||||
|                     doc.add(new NumericField(FIELD_YEAR, Field.Store.NO, true).setIntValue(mediaFile.getYear())); | ||||
|                 } | ||||
|                 if (mediaFile.getFolder() != null) { | ||||
|                     doc.add(new Field(FIELD_FOLDER, mediaFile.getFolder(), Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS)); | ||||
|                 } | ||||
| 
 | ||||
|                 return doc; | ||||
|             } | ||||
|         }, | ||||
| 
 | ||||
|         ALBUM(new String[]{FIELD_ALBUM, FIELD_ARTIST, FIELD_FOLDER}, FIELD_ALBUM) { | ||||
|             @Override | ||||
|             public Document createDocument(MediaFile mediaFile) { | ||||
|                 Document doc = new Document(); | ||||
|                 doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(mediaFile.getId())); | ||||
| 
 | ||||
|                 if (mediaFile.getArtist() != null) { | ||||
|                     doc.add(new Field(FIELD_ARTIST, mediaFile.getArtist(), Field.Store.YES, Field.Index.ANALYZED)); | ||||
|                 } | ||||
|                 if (mediaFile.getAlbumName() != null) { | ||||
|                     doc.add(new Field(FIELD_ALBUM, mediaFile.getAlbumName(), Field.Store.YES, Field.Index.ANALYZED)); | ||||
|                 } | ||||
|                 if (mediaFile.getFolder() != null) { | ||||
|                     doc.add(new Field(FIELD_FOLDER, mediaFile.getFolder(), Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS)); | ||||
|                 } | ||||
| 
 | ||||
|                 return doc; | ||||
|             } | ||||
|         }, | ||||
| 
 | ||||
|         ALBUM_ID3(new String[]{FIELD_ALBUM, FIELD_ARTIST, FIELD_FOLDER_ID}, FIELD_ALBUM) { | ||||
|             @Override | ||||
|             public Document createDocument(Album album) { | ||||
|                 Document doc = new Document(); | ||||
|                 doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(album.getId())); | ||||
| 
 | ||||
|                 if (album.getArtist() != null) { | ||||
|                     doc.add(new Field(FIELD_ARTIST, album.getArtist(), Field.Store.YES, Field.Index.ANALYZED)); | ||||
|                 } | ||||
|                 if (album.getName() != null) { | ||||
|                     doc.add(new Field(FIELD_ALBUM, album.getName(), Field.Store.YES, Field.Index.ANALYZED)); | ||||
|                 } | ||||
|                 if (album.getFolderId() != null) { | ||||
|                     doc.add(new NumericField(FIELD_FOLDER_ID, Field.Store.NO, true).setIntValue(album.getFolderId())); | ||||
|                 } | ||||
| 
 | ||||
|                 return doc; | ||||
|             } | ||||
|         }, | ||||
| 
 | ||||
|         ARTIST(new String[]{FIELD_ARTIST, FIELD_FOLDER}, null) { | ||||
|             @Override | ||||
|             public Document createDocument(MediaFile mediaFile) { | ||||
|                 Document doc = new Document(); | ||||
|                 doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(mediaFile.getId())); | ||||
| 
 | ||||
|                 if (mediaFile.getArtist() != null) { | ||||
|                     doc.add(new Field(FIELD_ARTIST, mediaFile.getArtist(), Field.Store.YES, Field.Index.ANALYZED)); | ||||
|                 } | ||||
|                 if (mediaFile.getFolder() != null) { | ||||
|                     doc.add(new Field(FIELD_FOLDER, mediaFile.getFolder(), Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS)); | ||||
|                 } | ||||
| 
 | ||||
|                 return doc; | ||||
|             } | ||||
|         }, | ||||
| 
 | ||||
|         ARTIST_ID3(new String[]{FIELD_ARTIST}, null) { | ||||
|             @Override | ||||
|             public Document createDocument(Artist artist, MusicFolder musicFolder) { | ||||
|                 Document doc = new Document(); | ||||
|                 doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(artist.getId())); | ||||
|                 doc.add(new Field(FIELD_ARTIST, artist.getName(), Field.Store.YES, Field.Index.ANALYZED)); | ||||
|                 doc.add(new NumericField(FIELD_FOLDER_ID, Field.Store.NO, true).setIntValue(musicFolder.getId())); | ||||
| 
 | ||||
|                 return doc; | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         private final String[] fields; | ||||
|         private final Map<String, Float> boosts; | ||||
| 
 | ||||
|         private IndexType(String[] fields, String boostedField) { | ||||
|             this.fields = fields; | ||||
|             boosts = new HashMap<String, Float>(); | ||||
|             if (boostedField != null) { | ||||
|                 boosts.put(boostedField, 2.0F); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public String[] getFields() { | ||||
|             return fields; | ||||
|         } | ||||
| 
 | ||||
|         protected Document createDocument(MediaFile mediaFile) { | ||||
|             throw new UnsupportedOperationException(); | ||||
|         } | ||||
| 
 | ||||
|         protected Document createDocument(Artist artist, MusicFolder musicFolder) { | ||||
|             throw new UnsupportedOperationException(); | ||||
|         } | ||||
| 
 | ||||
|         protected Document createDocument(Album album) { | ||||
|             throw new UnsupportedOperationException(); | ||||
|         } | ||||
| 
 | ||||
|         public Map<String, Float> getBoosts() { | ||||
|             return boosts; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private class CustomAnalyzer extends StandardAnalyzer { | ||||
|         private CustomAnalyzer() { | ||||
|             super(LUCENE_VERSION); | ||||
|         } | ||||
| 
 | ||||
|         @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(LUCENE_VERSION, 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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
|  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; | ||||
| 
 | ||||
| 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.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.search.IndexType; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| /** | ||||
|  * Performs Lucene-based searching and indexing. | ||||
|  * | ||||
|  * @author Sindre Mehus | ||||
|  * @version $Id$ | ||||
|  * @see MediaScannerService | ||||
|  */ | ||||
| public interface SearchService { | ||||
| 
 | ||||
|     void startIndexing(); | ||||
| 
 | ||||
|     void index(MediaFile mediaFile); | ||||
| 
 | ||||
|     void index(Artist artist, MusicFolder musicFolder); | ||||
| 
 | ||||
|     void index(Album album); | ||||
| 
 | ||||
|     void stopIndexing(); | ||||
| 
 | ||||
|     SearchResult search(SearchCriteria criteria, List<MusicFolder> musicFolders, | ||||
|             IndexType indexType); | ||||
| 
 | ||||
|     /** | ||||
|      * Returns a number of random songs. | ||||
|      * | ||||
|      * @param criteria Search criteria. | ||||
|      * @return List of random songs. | ||||
|      */ | ||||
|     List<MediaFile> getRandomSongs(RandomSearchCriteria criteria); | ||||
| 
 | ||||
|     /** | ||||
|      * Returns a number of random albums. | ||||
|      * | ||||
|      * @param count Number of albums to return. | ||||
|      * @param musicFolders Only return albums from these folders. | ||||
|      * @return List of random albums. | ||||
|      */ | ||||
|     List<MediaFile> getRandomAlbums(int count, List<MusicFolder> musicFolders); | ||||
| 
 | ||||
|     /** | ||||
|      * Returns a number of random albums, using ID3 tag. | ||||
|      * | ||||
|      * @param count Number of albums to return. | ||||
|      * @param musicFolders Only return albums from these folders. | ||||
|      * @return List of random albums. | ||||
|      */ | ||||
|     List<Album> getRandomAlbumsId3(int count, List<MusicFolder> musicFolders); | ||||
| 
 | ||||
|     <T> ParamSearchResult<T> searchByName( | ||||
|             String name, int offset, int count, List<MusicFolder> folderList, Class<T> clazz); | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -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