Signed-off-by: Andrew DeMaria <lostonamountain@gmail.com>master
parent
8e279a2d2a
commit
5c3c558923
@ -1,181 +1,235 @@ |
||||
/* |
||||
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; |
||||
} |
||||
|
||||
} |
||||
/* |
||||
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.Store; |
||||
import org.apache.lucene.document.FieldType; |
||||
import org.apache.lucene.document.IntPoint; |
||||
import org.apache.lucene.document.SortedDocValuesField; |
||||
import org.apache.lucene.document.StoredField; |
||||
import org.apache.lucene.document.TextField; |
||||
import org.apache.lucene.index.IndexOptions; |
||||
import org.apache.lucene.index.Term; |
||||
import org.apache.lucene.util.BytesRef; |
||||
import org.checkerframework.checker.nullness.qual.NonNull; |
||||
import org.checkerframework.checker.nullness.qual.Nullable; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import java.util.function.BiConsumer; |
||||
|
||||
import static org.springframework.util.ObjectUtils.isEmpty; |
||||
|
||||
/** |
||||
* A factory that generates the documents to be stored in the index. |
||||
*/ |
||||
@Component |
||||
public class DocumentFactory { |
||||
|
||||
private static final FieldType TYPE_ID; |
||||
|
||||
private static final FieldType TYPE_ID_NO_STORE; |
||||
|
||||
private static final FieldType TYPE_KEY; |
||||
|
||||
static { |
||||
|
||||
TYPE_ID = new FieldType(); |
||||
TYPE_ID.setIndexOptions(IndexOptions.DOCS); |
||||
TYPE_ID.setTokenized(false); |
||||
TYPE_ID.setOmitNorms(true); |
||||
TYPE_ID.setStored(true); |
||||
TYPE_ID.freeze(); |
||||
|
||||
TYPE_ID_NO_STORE = new FieldType(); |
||||
TYPE_ID_NO_STORE.setIndexOptions(IndexOptions.DOCS); |
||||
TYPE_ID_NO_STORE.setTokenized(false); |
||||
TYPE_ID_NO_STORE.setOmitNorms(true); |
||||
TYPE_ID_NO_STORE.setStored(false); |
||||
TYPE_ID_NO_STORE.freeze(); |
||||
|
||||
TYPE_KEY = new FieldType(); |
||||
TYPE_KEY.setIndexOptions(IndexOptions.DOCS); |
||||
TYPE_KEY.setTokenized(false); |
||||
TYPE_KEY.setOmitNorms(true); |
||||
TYPE_KEY.setStored(false); |
||||
TYPE_KEY.freeze(); |
||||
|
||||
} |
||||
|
||||
@FunctionalInterface |
||||
private interface Consumer<T, U, V> { |
||||
void accept(T t, U u, V v); |
||||
|
||||
} |
||||
|
||||
; |
||||
|
||||
private BiConsumer<@NonNull Document, @NonNull Integer> fieldId = (doc, value) -> { |
||||
doc.add(new StoredField(FieldNames.ID, Integer.toString(value), TYPE_ID)); |
||||
}; |
||||
|
||||
private BiConsumer<@NonNull Document, @NonNull Integer> fieldFolderId = (doc, value) -> { |
||||
doc.add(new StoredField(FieldNames.FOLDER_ID, Integer.toString(value), TYPE_ID_NO_STORE)); |
||||
}; |
||||
|
||||
private Consumer<@NonNull Document, @NonNull String, @NonNull String> fieldKey = (doc, field, value) -> { |
||||
doc.add(new StoredField(field, value, TYPE_KEY)); |
||||
}; |
||||
|
||||
private BiConsumer<@NonNull Document, @NonNull String> fieldMediatype = (doc, value) -> |
||||
fieldKey.accept(doc, FieldNames.MEDIA_TYPE, value); |
||||
|
||||
private BiConsumer<@NonNull Document, @NonNull String> fieldFolderPath = (doc, value) -> |
||||
fieldKey.accept(doc, FieldNames.FOLDER, value); |
||||
|
||||
private BiConsumer<@NonNull Document, @Nullable String> fieldGenre = (doc, value) -> { |
||||
if (isEmpty(value)) { |
||||
return; |
||||
} |
||||
fieldKey.accept(doc, FieldNames.GENRE, value); |
||||
}; |
||||
|
||||
private Consumer<@NonNull Document, @NonNull String, @Nullable Integer> fieldYear = (doc, fieldName, value) -> { |
||||
if (isEmpty(value)) { |
||||
return; |
||||
} |
||||
doc.add(new IntPoint(fieldName, value)); |
||||
}; |
||||
|
||||
private Consumer<@NonNull Document, @NonNull String, @Nullable String> fieldWords = (doc, fieldName, value) -> { |
||||
if (isEmpty(value)) { |
||||
return; |
||||
} |
||||
doc.add(new TextField(fieldName, value, Store.NO)); |
||||
doc.add(new SortedDocValuesField(fieldName, new BytesRef(value))); |
||||
}; |
||||
|
||||
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())); |
||||
}; |
||||
|
||||
/** |
||||
* Create a document. |
||||
* |
||||
* @param mediaFile target of document |
||||
* @return document |
||||
* @since legacy |
||||
*/ |
||||
public Document createAlbumDocument(MediaFile mediaFile) { |
||||
Document doc = new Document(); |
||||
fieldId.accept(doc, mediaFile.getId()); |
||||
fieldWords.accept(doc, FieldNames.ARTIST, mediaFile.getArtist()); |
||||
fieldWords.accept(doc, FieldNames.ALBUM, mediaFile.getAlbumName()); |
||||
fieldFolderPath.accept(doc, mediaFile.getFolder()); |
||||
return doc; |
||||
} |
||||
|
||||
/** |
||||
* Create a document. |
||||
* |
||||
* @param mediaFile target of document |
||||
* @return document |
||||
* @since legacy |
||||
*/ |
||||
public Document createArtistDocument(MediaFile mediaFile) { |
||||
Document doc = new Document(); |
||||
fieldId.accept(doc, mediaFile.getId()); |
||||
fieldWords.accept(doc, FieldNames.ARTIST, mediaFile.getArtist()); |
||||
fieldFolderPath.accept(doc, mediaFile.getFolder()); |
||||
return doc; |
||||
} |
||||
|
||||
/** |
||||
* Create a document. |
||||
* |
||||
* @param album target of document |
||||
* @return document |
||||
* @since legacy |
||||
*/ |
||||
public Document createAlbumId3Document(Album album) { |
||||
Document doc = new Document(); |
||||
fieldId.accept(doc, album.getId()); |
||||
fieldWords.accept(doc, FieldNames.ARTIST, album.getArtist()); |
||||
fieldWords.accept(doc, FieldNames.ALBUM, album.getName()); |
||||
fieldFolderId.accept(doc, album.getFolderId()); |
||||
return doc; |
||||
} |
||||
|
||||
/** |
||||
* Create a document. |
||||
* |
||||
* @param artist target of document |
||||
* @param musicFolder target folder exists |
||||
* @return document |
||||
* @since legacy |
||||
*/ |
||||
/* |
||||
* XXX 3.x -> 8.x : |
||||
* Only null check specification of createArtistId3Document is different from legacy. |
||||
* (The reason is only to simplify the function.) |
||||
* |
||||
* Since the field of domain object Album is nonnull, |
||||
* null check was not performed. |
||||
* |
||||
* In implementation ARTIST and ALBUM became nullable, |
||||
* but null is not input at this point in data flow. |
||||
*/ |
||||
public Document createArtistId3Document(Artist artist, MusicFolder musicFolder) { |
||||
Document doc = new Document(); |
||||
fieldId.accept(doc, artist.getId()); |
||||
fieldWords.accept(doc, FieldNames.ARTIST, artist.getName()); |
||||
fieldFolderId.accept(doc, 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(); |
||||
fieldId.accept(doc, mediaFile.getId()); |
||||
fieldMediatype.accept(doc, mediaFile.getMediaType().name()); |
||||
fieldWords.accept(doc, FieldNames.TITLE, mediaFile.getTitle()); |
||||
fieldWords.accept(doc, FieldNames.ARTIST, mediaFile.getArtist()); |
||||
fieldGenre.accept(doc, mediaFile.getGenre()); |
||||
fieldYear.accept(doc, FieldNames.YEAR, mediaFile.getYear()); |
||||
fieldFolderPath.accept(doc, mediaFile.getFolder()); |
||||
return doc; |
||||
} |
||||
|
||||
} |
||||
|
@ -1,95 +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"; |
||||
|
||||
} |
||||
/* |
||||
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"; |
||||
|
||||
} |
||||
|
@ -1,169 +1,323 @@ |
||||
/* |
||||
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); |
||||
} |
||||
} |
||||
|
||||
} |
||||
/* |
||||
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.commons.io.FileUtils; |
||||
import org.apache.lucene.document.Document; |
||||
import org.apache.lucene.index.IndexWriter; |
||||
import org.apache.lucene.index.IndexWriterConfig; |
||||
import org.apache.lucene.index.Term; |
||||
import org.apache.lucene.search.IndexSearcher; |
||||
import org.apache.lucene.search.SearcherManager; |
||||
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.Component; |
||||
|
||||
import java.io.File; |
||||
import java.io.IOException; |
||||
import java.util.Arrays; |
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
import java.util.function.Function; |
||||
import java.util.function.Supplier; |
||||
import java.util.regex.Pattern; |
||||
|
||||
import static org.springframework.util.ObjectUtils.isEmpty; |
||||
|
||||
/** |
||||
* 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); |
||||
|
||||
/** |
||||
* Schema version of Airsonic index. |
||||
* It may be incremented in the following cases: |
||||
* |
||||
* - Incompatible update case in Lucene index implementation |
||||
* - When schema definition is changed due to modification of AnalyzerFactory, |
||||
* DocumentFactory or the class that they use. |
||||
* |
||||
*/ |
||||
private static final int INDEX_VERSION = 16; |
||||
|
||||
/** |
||||
* Literal name of index top directory. |
||||
*/ |
||||
private static final String INDEX_ROOT_DIR_NAME = "index"; |
||||
|
||||
/** |
||||
* File supplier for index directory. |
||||
*/ |
||||
private Supplier<File> rootIndexDirectory = () -> |
||||
new File(SettingsService.getAirsonicHome(), INDEX_ROOT_DIR_NAME.concat(Integer.toString(INDEX_VERSION))); |
||||
|
||||
/** |
||||
* Returns the directory of the specified index |
||||
*/ |
||||
private Function<IndexType, File> getIndexDirectory = (indexType) -> |
||||
new File(rootIndexDirectory.get(), indexType.toString().toLowerCase()); |
||||
|
||||
@Autowired |
||||
private AnalyzerFactory analyzerFactory; |
||||
|
||||
@Autowired |
||||
private DocumentFactory documentFactory; |
||||
|
||||
private Map<IndexType, SearcherManager> searchers = new HashMap<>(); |
||||
|
||||
private Map<IndexType, IndexWriter> writers = new HashMap<>(); |
||||
|
||||
public void index(Album album) { |
||||
Term primarykey = documentFactory.createPrimarykey(album); |
||||
Document document = documentFactory.createAlbumId3Document(album); |
||||
try { |
||||
writers.get(IndexType.ALBUM_ID3).updateDocument(primarykey, document); |
||||
} catch (Exception x) { |
||||
LOG.error("Failed to create search index for " + album, x); |
||||
} |
||||
} |
||||
|
||||
public void index(Artist artist, MusicFolder musicFolder) { |
||||
Term primarykey = documentFactory.createPrimarykey(artist); |
||||
Document document = documentFactory.createArtistId3Document(artist, musicFolder); |
||||
try { |
||||
writers.get(IndexType.ARTIST_ID3).updateDocument(primarykey, document); |
||||
} catch (Exception x) { |
||||
LOG.error("Failed to create search index for " + artist, x); |
||||
} |
||||
} |
||||
|
||||
public void index(MediaFile mediaFile) { |
||||
Term primarykey = documentFactory.createPrimarykey(mediaFile); |
||||
try { |
||||
if (mediaFile.isFile()) { |
||||
Document document = documentFactory.createSongDocument(mediaFile); |
||||
writers.get(IndexType.SONG).updateDocument(primarykey, document); |
||||
} else if (mediaFile.isAlbum()) { |
||||
Document document = documentFactory.createAlbumDocument(mediaFile); |
||||
writers.get(IndexType.ALBUM).updateDocument(primarykey, document); |
||||
} else { |
||||
Document document = documentFactory.createArtistDocument(mediaFile); |
||||
writers.get(IndexType.ARTIST).updateDocument(primarykey, document); |
||||
} |
||||
} catch (Exception x) { |
||||
LOG.error("Failed to create search index for " + mediaFile, x); |
||||
} |
||||
} |
||||
|
||||
public final void startIndexing() { |
||||
try { |
||||
for (IndexType IndexType : IndexType.values()) { |
||||
writers.put(IndexType, createIndexWriter(IndexType)); |
||||
} |
||||
} catch (IOException e) { |
||||
LOG.error("Failed to create search index.", e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param indexType |
||||
* @return |
||||
* @throws IOException |
||||
*/ |
||||
private IndexWriter createIndexWriter(IndexType indexType) throws IOException { |
||||
File indexDirectory = getIndexDirectory.apply(indexType); |
||||
IndexWriterConfig config = new IndexWriterConfig(analyzerFactory.getAnalyzer()); |
||||
return new IndexWriter(FSDirectory.open(indexDirectory.toPath()), config); |
||||
} |
||||
|
||||
/** |
||||
* Close Writer of all indexes and update SearcherManager. |
||||
* Called at the end of the Scan flow. |
||||
*/ |
||||
public void stopIndexing() { |
||||
Arrays.asList(IndexType.values()).forEach(this::stopIndexing); |
||||
} |
||||
|
||||
/** |
||||
* Close Writer of specified index and refresh SearcherManager. |
||||
* @param type |
||||
*/ |
||||
private void stopIndexing(IndexType type) { |
||||
|
||||
boolean isUpdate = false; |
||||
// close
|
||||
try { |
||||
isUpdate = -1 != writers.get(type).commit(); |
||||
writers.get(type).close(); |
||||
writers.remove(type); |
||||
LOG.trace("Success to create or update search index : [" + type + "]"); |
||||
} catch (IOException e) { |
||||
LOG.error("Failed to create search index.", e); |
||||
} finally { |
||||
FileUtil.closeQuietly(writers.get(type)); |
||||
} |
||||
|
||||
// refresh reader as index may have been written
|
||||
if (isUpdate && searchers.containsKey(type)) { |
||||
try { |
||||
searchers.get(type).maybeRefresh(); |
||||
LOG.trace("SearcherManager has been refreshed : [" + type + "]"); |
||||
} catch (IOException e) { |
||||
LOG.error("Failed to refresh SearcherManager : [" + type + "]", e); |
||||
searchers.remove(type); |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Return the IndexSearcher of the specified index. |
||||
* At initial startup, it may return null |
||||
* if the user performs any search before performing a scan. |
||||
* |
||||
* @param indexType |
||||
* @return |
||||
*/ |
||||
public @Nullable IndexSearcher getSearcher(IndexType indexType) { |
||||
if (!searchers.containsKey(indexType)) { |
||||
File indexDirectory = getIndexDirectory.apply(indexType); |
||||
try { |
||||
if (indexDirectory.exists()) { |
||||
SearcherManager manager = new SearcherManager(FSDirectory.open(indexDirectory.toPath()), null); |
||||
searchers.put(indexType, manager); |
||||
} else { |
||||
LOG.warn("{} does not exist. Please run a scan.", indexDirectory.getAbsolutePath()); |
||||
} |
||||
} catch (IOException e) { |
||||
LOG.error("Failed to initialize SearcherManager.", e); |
||||
} |
||||
} |
||||
try { |
||||
SearcherManager manager = searchers.get(indexType); |
||||
if (!isEmpty(manager)) { |
||||
return searchers.get(indexType).acquire(); |
||||
} |
||||
} catch (Exception e) { |
||||
LOG.warn("Failed to acquire IndexSearcher.", e); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
public void release(IndexType indexType, IndexSearcher indexSearcher) { |
||||
if (searchers.containsKey(indexType)) { |
||||
try { |
||||
searchers.get(indexType).release(indexSearcher); |
||||
} catch (IOException e) { |
||||
LOG.error("Failed to release IndexSearcher.", e); |
||||
searchers.remove(indexType); |
||||
} |
||||
} else { |
||||
// irregular case
|
||||
try { |
||||
indexSearcher.getIndexReader().close(); |
||||
} catch (Exception e) { |
||||
LOG.warn("Failed to release. IndexSearcher has been closed.", e); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Check the version of the index and clean it up if necessary. |
||||
* Legacy type indexes (files or directories starting with lucene) are deleted. |
||||
* If there is no index directory, initialize the directory. |
||||
* If the index directory exists and is not the current version, |
||||
* initialize the directory. |
||||
*/ |
||||
public void deleteOldIndexFiles() { |
||||
|
||||
// Delete legacy files unconditionally
|
||||
Arrays.stream(SettingsService.getAirsonicHome() |
||||
.listFiles((file, name) -> Pattern.compile("^lucene\\d+$").matcher(name).matches())).forEach(old -> { |
||||
if (FileUtil.exists(old)) { |
||||
LOG.info("Found legacy index file. Try to delete : {}", old.getAbsolutePath()); |
||||
try { |
||||
if (old.isFile()) { |
||||
FileUtils.deleteQuietly(old); |
||||
} else { |
||||
FileUtils.deleteDirectory(old); |
||||
} |
||||
} catch (IOException e) { |
||||
// Log only if failed
|
||||
LOG.warn("Failed to delete the legacy Index : ".concat(old.getAbsolutePath()), e); |
||||
} |
||||
} |
||||
}); |
||||
|
||||
// Delete if not old index version
|
||||
Arrays.stream(SettingsService.getAirsonicHome() |
||||
.listFiles((file, name) -> Pattern.compile("^index\\d+$").matcher(name).matches())) |
||||
.filter(dir -> !dir.getName().equals(rootIndexDirectory.get().getName())) |
||||
.forEach(old -> { |
||||
if (FileUtil.exists(old)) { |
||||
LOG.info("Found old index file. Try to delete : {}", old.getAbsolutePath()); |
||||
try { |
||||
if (old.isFile()) { |
||||
FileUtils.deleteQuietly(old); |
||||
} else { |
||||
FileUtils.deleteDirectory(old); |
||||
} |
||||
} catch (IOException e) { |
||||
// Log only if failed
|
||||
LOG.warn("Failed to delete the old Index : ".concat(old.getAbsolutePath()), e); |
||||
} |
||||
} |
||||
}); |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Create a directory corresponding to the current index version. |
||||
*/ |
||||
public void initializeIndexDirectory() { |
||||
// Check if Index is current version
|
||||
if (rootIndexDirectory.get().exists()) { |
||||
// Index of current version already exists
|
||||
LOG.info("Index was found (index version {}). ", INDEX_VERSION); |
||||
} else { |
||||
if (rootIndexDirectory.get().mkdir()) { |
||||
LOG.info("Index directory was created (index version {}). ", INDEX_VERSION); |
||||
} else { |
||||
LOG.warn("Failed to create index directory : (index version {}). ", INDEX_VERSION); |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
@ -1,231 +1,291 @@ |
||||
/* |
||||
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; |
||||
} |
||||
|
||||
} |
||||
/* |
||||
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.MediaType; |
||||
import org.airsonic.player.domain.MusicFolder; |
||||
import org.airsonic.player.domain.RandomSearchCriteria; |
||||
import org.airsonic.player.domain.SearchCriteria; |
||||
import org.apache.lucene.analysis.Analyzer; |
||||
import org.apache.lucene.analysis.TokenStream; |
||||
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; |
||||
import org.apache.lucene.document.IntPoint; |
||||
import org.apache.lucene.index.Term; |
||||
import org.apache.lucene.search.BooleanClause.Occur; |
||||
import org.apache.lucene.search.BooleanQuery; |
||||
import org.apache.lucene.search.Query; |
||||
import org.apache.lucene.search.TermQuery; |
||||
import org.apache.lucene.search.WildcardQuery; |
||||
import org.checkerframework.checker.nullness.qual.NonNull; |
||||
import org.checkerframework.checker.nullness.qual.Nullable; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import java.io.IOException; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
import java.util.function.BiFunction; |
||||
import java.util.function.Function; |
||||
|
||||
import static org.springframework.util.ObjectUtils.isEmpty; |
||||
|
||||
/** |
||||
* 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 { |
||||
|
||||
private static final String ASTERISK = "*"; |
||||
|
||||
@Autowired |
||||
private AnalyzerFactory analyzerFactory; |
||||
|
||||
private final Function<MusicFolder, Query> toFolderIdQuery = (folder) -> { |
||||
// Unanalyzed field
|
||||
return new TermQuery(new Term(FieldNames.FOLDER_ID, folder.getId().toString())); |
||||
}; |
||||
|
||||
private final Function<MusicFolder, Query> toFolderPathQuery = (folder) -> { |
||||
// Unanalyzed field
|
||||
return new TermQuery(new Term(FieldNames.FOLDER, folder.getPath().getPath())); |
||||
}; |
||||
|
||||
/* |
||||
* XXX 3.x -> 8.x : |
||||
* "SpanOr" has been changed to "Or". |
||||
* - Path comparison is more appropriate with "Or". |
||||
* - If "SpanOr" is maintained, the DOC design needs to be changed. |
||||
*/ |
||||
private final BiFunction<@NonNull Boolean, @NonNull List<MusicFolder>, @NonNull Query> toFolderQuery = ( |
||||
isId3, folders) -> { |
||||
BooleanQuery.Builder mfQuery = new BooleanQuery.Builder(); |
||||
folders.stream() |
||||
.map(isId3 ? toFolderIdQuery : toFolderPathQuery) |
||||
.forEach(t -> mfQuery.add(t, Occur.SHOULD)); |
||||
return mfQuery.build(); |
||||
}; |
||||
|
||||
/* |
||||
* XXX 3.x -> 8.x : |
||||
* In order to support wildcards, |
||||
* MultiFieldQueryParser has been replaced by the following process. |
||||
* |
||||
* - There is also an override of MultiFieldQueryParser, but it is known to be high cost. |
||||
* - MultiFieldQueryParser was created before Java API was modernized. |
||||
* - The spec of Parser has changed from time to time. Using parser does not reduce library update risk. |
||||
* - Self made parser process reduces one library dependency. |
||||
* - It is easy to make corrections later when changing the query to improve search accuracy. |
||||
*/ |
||||
private Query createMultiFieldWildQuery(@NonNull String[] fieldNames, @NonNull String queryString) |
||||
throws IOException { |
||||
|
||||
BooleanQuery.Builder mainQuery = new BooleanQuery.Builder(); |
||||
|
||||
List<List<Query>> fieldsQuerys = new ArrayList<>(); |
||||
Analyzer analyzer = analyzerFactory.getQueryAnalyzer(); |
||||
|
||||
/* Wildcard applies to all tokens. **/ |
||||
for (String fieldName : fieldNames) { |
||||
try (TokenStream stream = analyzer.tokenStream(fieldName, queryString)) { |
||||
stream.reset(); |
||||
List<Query> fieldQuerys = new ArrayList<>(); |
||||
while (stream.incrementToken()) { |
||||
String token = stream.getAttribute(CharTermAttribute.class).toString(); |
||||
WildcardQuery wildcardQuery = new WildcardQuery(new Term(fieldName, token.concat(ASTERISK))); |
||||
fieldQuerys.add(wildcardQuery); |
||||
} |
||||
fieldsQuerys.add(fieldQuerys); |
||||
} |
||||
} |
||||
|
||||
/* If Field's Tokenizer is different, token's length may not match. **/ |
||||
int maxTermLength = fieldsQuerys.stream() |
||||
.map(l -> l.size()) |
||||
.max(Integer::compare).get(); |
||||
|
||||
if (0 < fieldsQuerys.size()) { |
||||
for (int i = 0; i < maxTermLength; i++) { |
||||
BooleanQuery.Builder fieldsQuery = new BooleanQuery.Builder(); |
||||
for (List<Query> fieldQuerys : fieldsQuerys) { |
||||
if (i < fieldQuerys.size()) { |
||||
fieldsQuery.add(fieldQuerys.get(i), Occur.SHOULD); |
||||
} |
||||
} |
||||
mainQuery.add(fieldsQuery.build(), Occur.SHOULD); |
||||
} |
||||
} |
||||
|
||||
return mainQuery.build(); |
||||
|
||||
}; |
||||
|
||||
/* |
||||
* XXX 3.x -> 8.x : |
||||
* RangeQuery has been changed to not allow null. |
||||
*/ |
||||
private final BiFunction<@Nullable Integer, @Nullable Integer, @NonNull Query> toYearRangeQuery = |
||||
(from, to) -> { |
||||
return IntPoint.newRangeQuery(FieldNames.YEAR, |
||||
isEmpty(from) ? Integer.MIN_VALUE : from, |
||||
isEmpty(to) ? Integer.MAX_VALUE : to); |
||||
}; |
||||
|
||||
/** |
||||
* 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 |
||||
*/ |
||||
public Query search(SearchCriteria criteria, List<MusicFolder> musicFolders, |
||||
IndexType indexType) throws IOException { |
||||
|
||||
BooleanQuery.Builder mainQuery = new BooleanQuery.Builder(); |
||||
|
||||
Query multiFieldQuery = createMultiFieldWildQuery(indexType.getFields(), criteria.getQuery()); |
||||
mainQuery.add(multiFieldQuery, Occur.MUST); |
||||
|
||||
boolean isId3 = indexType == IndexType.ALBUM_ID3 || indexType == IndexType.ARTIST_ID3; |
||||
Query folderQuery = toFolderQuery.apply(isId3, musicFolders); |
||||
mainQuery.add(folderQuery, Occur.MUST); |
||||
|
||||
return mainQuery.build(); |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Query generation expression extracted from |
||||
* {@link org.airsonic.player.service.SearchService#getRandomSongs(RandomSearchCriteria)}. |
||||
* |
||||
* @param criteria criteria |
||||
* @return Query |
||||
* @throws IOException |
||||
*/ |
||||
public Query getRandomSongs(RandomSearchCriteria criteria) throws IOException { |
||||
|
||||
BooleanQuery.Builder query = new BooleanQuery.Builder(); |
||||
|
||||
Analyzer analyzer = analyzerFactory.getQueryAnalyzer(); |
||||
|
||||
// Unanalyzed field
|
||||
query.add(new TermQuery(new Term(FieldNames.MEDIA_TYPE, MediaType.MUSIC.name())), Occur.MUST); |
||||
|
||||
if (!isEmpty(criteria.getGenre())) { |
||||
|
||||
// Unanalyzed field, but performs filtering according to id3 tag parser.
|
||||
try (TokenStream stream = analyzer.tokenStream(FieldNames.GENRE, criteria.getGenre())) { |
||||
stream.reset(); |
||||
if (stream.incrementToken()) { |
||||
String token = stream.getAttribute(CharTermAttribute.class).toString(); |
||||
query.add(new TermQuery(new Term(FieldNames.GENRE, token)), Occur.MUST); |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (!(isEmpty(criteria.getFromYear()) && isEmpty(criteria.getToYear()))) { |
||||
query.add(toYearRangeQuery.apply(criteria.getFromYear(), criteria.getToYear()), Occur.MUST); |
||||
} |
||||
|
||||
query.add(toFolderQuery.apply(false, criteria.getMusicFolders()), Occur.MUST); |
||||
|
||||
return query.build(); |
||||
|
||||
} |
||||
|
||||
/** |
||||
* 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 IOException When parsing of QueryParser fails |
||||
*/ |
||||
public Query searchByName(String fieldName, String name) throws IOException { |
||||
|
||||
BooleanQuery.Builder mainQuery = new BooleanQuery.Builder(); |
||||
|
||||
Analyzer analyzer = analyzerFactory.getQueryAnalyzer(); |
||||
|
||||
try (TokenStream stream = analyzer.tokenStream(fieldName, name)) { |
||||
stream.reset(); |
||||
stream.incrementToken(); |
||||
|
||||
/* |
||||
* XXX 3.x -> 8.x : |
||||
* In order to support wildcards, |
||||
* QueryParser has been replaced by the following process. |
||||
*/ |
||||
|
||||
/* Wildcards apply only to tail tokens **/ |
||||
while (true) { |
||||
String token = stream.getAttribute(CharTermAttribute.class).toString(); |
||||
if (stream.incrementToken()) { |
||||
mainQuery.add(new TermQuery(new Term(fieldName, token)), Occur.SHOULD); |
||||
} else { |
||||
WildcardQuery wildcardQuery = new WildcardQuery(new Term(fieldName, token.concat(ASTERISK))); |
||||
mainQuery.add(wildcardQuery, Occur.SHOULD); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
return mainQuery.build(); |
||||
|
||||
} |
||||
|
||||
/** |
||||
* 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) { |
||||
return new BooleanQuery.Builder() |
||||
.add(toFolderQuery.apply(false, musicFolders), Occur.SHOULD) |
||||
.build(); |
||||
} |
||||
|
||||
/** |
||||
* 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) { |
||||
return new BooleanQuery.Builder() |
||||
.add(toFolderQuery.apply(true, musicFolders), Occur.SHOULD) |
||||
.build(); |
||||
} |
||||
|
||||
} |
||||
|
@ -1,200 +1,176 @@ |
||||
/* |
||||
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); |
||||
} |
||||
} |
||||
|
||||
} |
||||
/* |
||||
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.apache.commons.collections.CollectionUtils; |
||||
import org.apache.lucene.document.Document; |
||||
import org.checkerframework.checker.nullness.qual.Nullable; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import java.util.Collection; |
||||
import java.util.List; |
||||
import java.util.function.BiConsumer; |
||||
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); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
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); |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
@ -1,138 +1,138 @@ |
||||
package org.airsonic.player.service.search; |
||||
|
||||
import java.util.Map; |
||||
import java.util.concurrent.atomic.AtomicBoolean; |
||||
import java.util.function.Function; |
||||
|
||||
import org.airsonic.player.TestCaseUtils; |
||||
import org.airsonic.player.dao.DaoHelper; |
||||
import org.airsonic.player.dao.MusicFolderDao; |
||||
import org.airsonic.player.service.MediaScannerService; |
||||
import org.airsonic.player.service.SettingsService; |
||||
import org.airsonic.player.util.HomeRule; |
||||
import org.airsonic.player.util.MusicFolderTestData; |
||||
import org.junit.ClassRule; |
||||
import org.junit.Rule; |
||||
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.stereotype.Component; |
||||
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; |
||||
|
||||
@ContextConfiguration(locations = { |
||||
"/applicationContext-service.xml", |
||||
"/applicationContext-cache.xml", |
||||
"/applicationContext-testdb.xml"}) |
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) |
||||
@Component |
||||
/** |
||||
* Abstract class for scanning MusicFolder. |
||||
*/ |
||||
public abstract class AbstractAirsonicHomeTest implements AirsonicHomeTest { |
||||
|
||||
@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); |
||||
} |
||||
}; |
||||
|
||||
/* |
||||
* Currently, Maven is executing test classes in series, |
||||
* so this class can hold the state. |
||||
* When executing in parallel, subclasses should override this. |
||||
*/ |
||||
private static AtomicBoolean dataBasePopulated = new AtomicBoolean(); |
||||
|
||||
// Above.
|
||||
private static AtomicBoolean dataBaseReady = new AtomicBoolean(); |
||||
|
||||
protected final static Function<String, String> resolveBaseMediaPath = (childPath) -> { |
||||
return MusicFolderTestData.resolveBaseMediaPath().concat(childPath); |
||||
}; |
||||
|
||||
@Autowired |
||||
protected DaoHelper daoHelper; |
||||
|
||||
@Autowired |
||||
protected MediaScannerService mediaScannerService; |
||||
|
||||
@Autowired |
||||
protected MusicFolderDao musicFolderDao; |
||||
|
||||
@Autowired |
||||
protected SettingsService settingsService; |
||||
|
||||
@Rule |
||||
public final SpringMethodRule springMethodRule = new SpringMethodRule(); |
||||
|
||||
@Rule |
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder(); |
||||
|
||||
@Override |
||||
public AtomicBoolean dataBasePopulated() { |
||||
return dataBasePopulated; |
||||
} |
||||
|
||||
@Override |
||||
public AtomicBoolean dataBaseReady() { |
||||
return dataBaseReady; |
||||
} |
||||
|
||||
@Override |
||||
public final void populateDatabaseOnlyOnce() { |
||||
if (!dataBasePopulated().get()) { |
||||
dataBasePopulated().set(true); |
||||
getMusicFolders().forEach(musicFolderDao::createMusicFolder); |
||||
settingsService.clearMusicFolderCache(); |
||||
try { |
||||
// Await time to avoid scan failure.
|
||||
for (int i = 0; i < 10; i++) { |
||||
Thread.sleep(100); |
||||
} |
||||
} catch (InterruptedException e) { |
||||
e.printStackTrace(); |
||||
} |
||||
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") |
||||
| s.equals("ARTIST") |
||||
| s.equals("MUSIC_FOLDER") |
||||
| s.equals("ALBUM")) |
||||
.forEach(tableName -> |
||||
System.out.println("\t" + tableName + " : " + records.get(tableName).toString())); |
||||
System.out.println("--- *********************** ---"); |
||||
try { |
||||
// Await for Lucene to finish writing(asynchronous).
|
||||
for (int i = 0; i < 5; i++) { |
||||
Thread.sleep(100); |
||||
} |
||||
} catch (InterruptedException e) { |
||||
e.printStackTrace(); |
||||
} |
||||
dataBaseReady().set(true); |
||||
} else { |
||||
while (!dataBaseReady().get()) { |
||||
try { |
||||
// The subsequent test method waits while reading DB data.
|
||||
for (int i = 0; i < 10; i++) { |
||||
Thread.sleep(100); |
||||
} |
||||
} catch (InterruptedException e) { |
||||
e.printStackTrace(); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
package org.airsonic.player.service.search; |
||||
|
||||
import java.util.Map; |
||||
import java.util.concurrent.atomic.AtomicBoolean; |
||||
import java.util.function.Function; |
||||
|
||||
import org.airsonic.player.TestCaseUtils; |
||||
import org.airsonic.player.dao.DaoHelper; |
||||
import org.airsonic.player.dao.MusicFolderDao; |
||||
import org.airsonic.player.service.MediaScannerService; |
||||
import org.airsonic.player.service.SettingsService; |
||||
import org.airsonic.player.util.HomeRule; |
||||
import org.airsonic.player.util.MusicFolderTestData; |
||||
import org.junit.ClassRule; |
||||
import org.junit.Rule; |
||||
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.stereotype.Component; |
||||
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; |
||||
|
||||
@ContextConfiguration(locations = { |
||||
"/applicationContext-service.xml", |
||||
"/applicationContext-cache.xml", |
||||
"/applicationContext-testdb.xml"}) |
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) |
||||
@Component |
||||
/** |
||||
* Abstract class for scanning MusicFolder. |
||||
*/ |
||||
public abstract class AbstractAirsonicHomeTest implements AirsonicHomeTest { |
||||
|
||||
@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); |
||||
} |
||||
}; |
||||
|
||||
/* |
||||
* Currently, Maven is executing test classes in series, |
||||
* so this class can hold the state. |
||||
* When executing in parallel, subclasses should override this. |
||||
*/ |
||||
private static AtomicBoolean dataBasePopulated = new AtomicBoolean(); |
||||
|
||||
// Above.
|
||||
private static AtomicBoolean dataBaseReady = new AtomicBoolean(); |
||||
|
||||
protected final static Function<String, String> resolveBaseMediaPath = (childPath) -> { |
||||
return MusicFolderTestData.resolveBaseMediaPath().concat(childPath); |
||||
}; |
||||
|
||||
@Autowired |
||||
protected DaoHelper daoHelper; |
||||
|
||||
@Autowired |
||||
protected MediaScannerService mediaScannerService; |
||||
|
||||
@Autowired |
||||
protected MusicFolderDao musicFolderDao; |
||||
|
||||
@Autowired |
||||
protected SettingsService settingsService; |
||||
|
||||
@Rule |
||||
public final SpringMethodRule springMethodRule = new SpringMethodRule(); |
||||
|
||||
@Rule |
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder(); |
||||
|
||||
@Override |
||||
public AtomicBoolean dataBasePopulated() { |
||||
return dataBasePopulated; |
||||
} |
||||
|
||||
@Override |
||||
public AtomicBoolean dataBaseReady() { |
||||
return dataBaseReady; |
||||
} |
||||
|
||||
@Override |
||||
public final void populateDatabaseOnlyOnce() { |
||||
if (!dataBasePopulated().get()) { |
||||
dataBasePopulated().set(true); |
||||
getMusicFolders().forEach(musicFolderDao::createMusicFolder); |
||||
settingsService.clearMusicFolderCache(); |
||||
try { |
||||
// Await time to avoid scan failure.
|
||||
for (int i = 0; i < 10; i++) { |
||||
Thread.sleep(100); |
||||
} |
||||
} catch (InterruptedException e) { |
||||
e.printStackTrace(); |
||||
} |
||||
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") |
||||
| s.equals("ARTIST") |
||||
| s.equals("MUSIC_FOLDER") |
||||
| s.equals("ALBUM")) |
||||
.forEach(tableName -> |
||||
System.out.println("\t" + tableName + " : " + records.get(tableName).toString())); |
||||
System.out.println("--- *********************** ---"); |
||||
try { |
||||
// Await for Lucene to finish writing(asynchronous).
|
||||
for (int i = 0; i < 5; i++) { |
||||
Thread.sleep(100); |
||||
} |
||||
} catch (InterruptedException e) { |
||||
e.printStackTrace(); |
||||
} |
||||
dataBaseReady().set(true); |
||||
} else { |
||||
while (!dataBaseReady().get()) { |
||||
try { |
||||
// The subsequent test method waits while reading DB data.
|
||||
for (int i = 0; i < 10; i++) { |
||||
Thread.sleep(100); |
||||
} |
||||
} catch (InterruptedException e) { |
||||
e.printStackTrace(); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,45 +1,45 @@ |
||||
package org.airsonic.player.service.search; |
||||
|
||||
import java.util.List; |
||||
import java.util.concurrent.atomic.AtomicBoolean; |
||||
|
||||
import org.airsonic.player.domain.MusicFolder; |
||||
import org.airsonic.player.util.MusicFolderTestData; |
||||
|
||||
/** |
||||
* Test case interface for scanning MusicFolder. |
||||
*/ |
||||
public interface AirsonicHomeTest { |
||||
|
||||
/** |
||||
* MusicFolder used by test class. |
||||
* |
||||
* @return MusicFolder used by test class
|
||||
*/ |
||||
default List<MusicFolder> getMusicFolders() { |
||||
return MusicFolderTestData.getTestMusicFolders(); |
||||
}; |
||||
|
||||
/** |
||||
* Whether the data input has been completed. |
||||
* |
||||
* @return Static AtomicBoolean indicating whether the data injection has been |
||||
* completed |
||||
*/ |
||||
abstract AtomicBoolean dataBasePopulated(); |
||||
|
||||
/** |
||||
* Whether the data input has been completed. |
||||
* |
||||
* @return Static AtomicBoolean indicating whether the data injection has been |
||||
* completed |
||||
*/ |
||||
abstract AtomicBoolean dataBaseReady(); |
||||
|
||||
/** |
||||
* Populate the database only once. |
||||
* It is called in the @Before granted method. |
||||
*/ |
||||
void populateDatabaseOnlyOnce(); |
||||
|
||||
} |
||||
package org.airsonic.player.service.search; |
||||
|
||||
import java.util.List; |
||||
import java.util.concurrent.atomic.AtomicBoolean; |
||||
|
||||
import org.airsonic.player.domain.MusicFolder; |
||||
import org.airsonic.player.util.MusicFolderTestData; |
||||
|
||||
/** |
||||
* Test case interface for scanning MusicFolder. |
||||
*/ |
||||
public interface AirsonicHomeTest { |
||||
|
||||
/** |
||||
* MusicFolder used by test class. |
||||
* |
||||
* @return MusicFolder used by test class
|
||||
*/ |
||||
default List<MusicFolder> getMusicFolders() { |
||||
return MusicFolderTestData.getTestMusicFolders(); |
||||
}; |
||||
|
||||
/** |
||||
* Whether the data input has been completed. |
||||
* |
||||
* @return Static AtomicBoolean indicating whether the data injection has been |
||||
* completed |
||||
*/ |
||||
abstract AtomicBoolean dataBasePopulated(); |
||||
|
||||
/** |
||||
* Whether the data input has been completed. |
||||
* |
||||
* @return Static AtomicBoolean indicating whether the data injection has been |
||||
* completed |
||||
*/ |
||||
abstract AtomicBoolean dataBaseReady(); |
||||
|
||||
/** |
||||
* Populate the database only once. |
||||
* It is called in the @Before granted method. |
||||
*/ |
||||
void populateDatabaseOnlyOnce(); |
||||
|
||||
} |
||||
|
Loading…
Reference in new issue