Signed-off-by: Andrew DeMaria <lostonamountain@gmail.com>master
parent
8e279a2d2a
commit
5c3c558923
@ -1,181 +1,235 @@ |
|||||||
/* |
/* |
||||||
This file is part of Airsonic. |
This file is part of Airsonic. |
||||||
|
|
||||||
Airsonic is free software: you can redistribute it and/or modify |
Airsonic is free software: you can redistribute it and/or modify |
||||||
it under the terms of the GNU General Public License as published by |
it under the terms of the GNU General Public License as published by |
||||||
the Free Software Foundation, either version 3 of the License, or |
the Free Software Foundation, either version 3 of the License, or |
||||||
(at your option) any later version. |
(at your option) any later version. |
||||||
|
|
||||||
Airsonic is distributed in the hope that it will be useful, |
Airsonic is distributed in the hope that it will be useful, |
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
GNU General Public License for more details. |
GNU General Public License for more details. |
||||||
|
|
||||||
You should have received a copy of the GNU General Public License |
You should have received a copy of the GNU General Public License |
||||||
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Copyright 2016 (C) Airsonic Authors |
Copyright 2016 (C) Airsonic Authors |
||||||
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus |
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus |
||||||
*/ |
*/ |
||||||
|
|
||||||
package org.airsonic.player.service.search; |
package org.airsonic.player.service.search; |
||||||
|
|
||||||
import org.airsonic.player.domain.Album; |
import org.airsonic.player.domain.Album; |
||||||
import org.airsonic.player.domain.Artist; |
import org.airsonic.player.domain.Artist; |
||||||
import org.airsonic.player.domain.MediaFile; |
import org.airsonic.player.domain.MediaFile; |
||||||
import org.airsonic.player.domain.MusicFolder; |
import org.airsonic.player.domain.MusicFolder; |
||||||
import org.apache.lucene.document.Document; |
import org.apache.lucene.document.Document; |
||||||
import org.apache.lucene.document.Field; |
import org.apache.lucene.document.Field.Store; |
||||||
import org.apache.lucene.document.NumericField; |
import org.apache.lucene.document.FieldType; |
||||||
import org.springframework.stereotype.Component; |
import org.apache.lucene.document.IntPoint; |
||||||
|
import org.apache.lucene.document.SortedDocValuesField; |
||||||
/** |
import org.apache.lucene.document.StoredField; |
||||||
* A factory that generates the documents to be stored in the index. |
import org.apache.lucene.document.TextField; |
||||||
*/ |
import org.apache.lucene.index.IndexOptions; |
||||||
@Component |
import org.apache.lucene.index.Term; |
||||||
public class DocumentFactory { |
import org.apache.lucene.util.BytesRef; |
||||||
|
import org.checkerframework.checker.nullness.qual.NonNull; |
||||||
/** |
import org.checkerframework.checker.nullness.qual.Nullable; |
||||||
* Normalize the genre string. |
import org.springframework.stereotype.Component; |
||||||
* |
|
||||||
* @param genre genre string |
import java.util.function.BiConsumer; |
||||||
* @return genre string normalized |
|
||||||
* @deprecated should be resolved with tokenizer or filter |
import static org.springframework.util.ObjectUtils.isEmpty; |
||||||
*/ |
|
||||||
@Deprecated |
/** |
||||||
private String normalizeGenre(String genre) { |
* A factory that generates the documents to be stored in the index. |
||||||
return genre.toLowerCase().replace(" ", "").replace("-", ""); |
*/ |
||||||
} |
@Component |
||||||
|
public class DocumentFactory { |
||||||
/** |
|
||||||
* Create a document. |
private static final FieldType TYPE_ID; |
||||||
* |
|
||||||
* @param mediaFile target of document |
private static final FieldType TYPE_ID_NO_STORE; |
||||||
* @return document |
|
||||||
* @since legacy |
private static final FieldType TYPE_KEY; |
||||||
*/ |
|
||||||
public Document createAlbumDocument(MediaFile mediaFile) { |
static { |
||||||
Document doc = new Document(); |
|
||||||
doc.add(new NumericField(FieldNames.ID, Field.Store.YES, false) |
TYPE_ID = new FieldType(); |
||||||
.setIntValue(mediaFile.getId())); |
TYPE_ID.setIndexOptions(IndexOptions.DOCS); |
||||||
|
TYPE_ID.setTokenized(false); |
||||||
if (mediaFile.getArtist() != null) { |
TYPE_ID.setOmitNorms(true); |
||||||
doc.add(new Field(FieldNames.ARTIST, mediaFile.getArtist(), Field.Store.YES, |
TYPE_ID.setStored(true); |
||||||
Field.Index.ANALYZED)); |
TYPE_ID.freeze(); |
||||||
} |
|
||||||
if (mediaFile.getAlbumName() != null) { |
TYPE_ID_NO_STORE = new FieldType(); |
||||||
doc.add(new Field(FieldNames.ALBUM, mediaFile.getAlbumName(), Field.Store.YES, |
TYPE_ID_NO_STORE.setIndexOptions(IndexOptions.DOCS); |
||||||
Field.Index.ANALYZED)); |
TYPE_ID_NO_STORE.setTokenized(false); |
||||||
} |
TYPE_ID_NO_STORE.setOmitNorms(true); |
||||||
if (mediaFile.getFolder() != null) { |
TYPE_ID_NO_STORE.setStored(false); |
||||||
doc.add(new Field(FieldNames.FOLDER, mediaFile.getFolder(), Field.Store.NO, |
TYPE_ID_NO_STORE.freeze(); |
||||||
Field.Index.NOT_ANALYZED_NO_NORMS)); |
|
||||||
} |
TYPE_KEY = new FieldType(); |
||||||
return doc; |
TYPE_KEY.setIndexOptions(IndexOptions.DOCS); |
||||||
} |
TYPE_KEY.setTokenized(false); |
||||||
|
TYPE_KEY.setOmitNorms(true); |
||||||
/** |
TYPE_KEY.setStored(false); |
||||||
* Create a document. |
TYPE_KEY.freeze(); |
||||||
* |
|
||||||
* @param mediaFile target of document |
} |
||||||
* @return document |
|
||||||
* @since legacy |
@FunctionalInterface |
||||||
*/ |
private interface Consumer<T, U, V> { |
||||||
public Document createArtistDocument(MediaFile mediaFile) { |
void accept(T t, U u, V v); |
||||||
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, |
private BiConsumer<@NonNull Document, @NonNull Integer> fieldId = (doc, value) -> { |
||||||
Field.Index.ANALYZED)); |
doc.add(new StoredField(FieldNames.ID, Integer.toString(value), TYPE_ID)); |
||||||
} |
}; |
||||||
if (mediaFile.getFolder() != null) { |
|
||||||
doc.add(new Field(FieldNames.FOLDER, mediaFile.getFolder(), Field.Store.NO, |
private BiConsumer<@NonNull Document, @NonNull Integer> fieldFolderId = (doc, value) -> { |
||||||
Field.Index.NOT_ANALYZED_NO_NORMS)); |
doc.add(new StoredField(FieldNames.FOLDER_ID, Integer.toString(value), TYPE_ID_NO_STORE)); |
||||||
} |
}; |
||||||
return doc; |
|
||||||
} |
private Consumer<@NonNull Document, @NonNull String, @NonNull String> fieldKey = (doc, field, value) -> { |
||||||
|
doc.add(new StoredField(field, value, TYPE_KEY)); |
||||||
/** |
}; |
||||||
* Create a document. |
|
||||||
* |
private BiConsumer<@NonNull Document, @NonNull String> fieldMediatype = (doc, value) -> |
||||||
* @param album target of document |
fieldKey.accept(doc, FieldNames.MEDIA_TYPE, value); |
||||||
* @return document |
|
||||||
* @since legacy |
private BiConsumer<@NonNull Document, @NonNull String> fieldFolderPath = (doc, value) -> |
||||||
*/ |
fieldKey.accept(doc, FieldNames.FOLDER, value); |
||||||
public Document createAlbumId3Document(Album album) { |
|
||||||
Document doc = new Document(); |
private BiConsumer<@NonNull Document, @Nullable String> fieldGenre = (doc, value) -> { |
||||||
doc.add(new NumericField(FieldNames.ID, Field.Store.YES, false).setIntValue(album.getId())); |
if (isEmpty(value)) { |
||||||
|
return; |
||||||
if (album.getArtist() != null) { |
} |
||||||
doc.add(new Field(FieldNames.ARTIST, album.getArtist(), Field.Store.YES, |
fieldKey.accept(doc, FieldNames.GENRE, value); |
||||||
Field.Index.ANALYZED)); |
}; |
||||||
} |
|
||||||
if (album.getName() != null) { |
private Consumer<@NonNull Document, @NonNull String, @Nullable Integer> fieldYear = (doc, fieldName, value) -> { |
||||||
doc.add(new Field(FieldNames.ALBUM, album.getName(), Field.Store.YES, |
if (isEmpty(value)) { |
||||||
Field.Index.ANALYZED)); |
return; |
||||||
} |
} |
||||||
if (album.getFolderId() != null) { |
doc.add(new IntPoint(fieldName, value)); |
||||||
doc.add(new NumericField(FieldNames.FOLDER_ID, Field.Store.NO, true) |
}; |
||||||
.setIntValue(album.getFolderId())); |
|
||||||
} |
private Consumer<@NonNull Document, @NonNull String, @Nullable String> fieldWords = (doc, fieldName, value) -> { |
||||||
return doc; |
if (isEmpty(value)) { |
||||||
} |
return; |
||||||
|
} |
||||||
/** |
doc.add(new TextField(fieldName, value, Store.NO)); |
||||||
* Create a document. |
doc.add(new SortedDocValuesField(fieldName, new BytesRef(value))); |
||||||
* |
}; |
||||||
* @param artist target of document |
|
||||||
* @param musicFolder target folder exists |
public final Term createPrimarykey(Album album) { |
||||||
* @return document |
return new Term(FieldNames.ID, Integer.toString(album.getId())); |
||||||
* @since legacy |
}; |
||||||
*/ |
|
||||||
public Document createArtistId3Document(Artist artist, MusicFolder musicFolder) { |
public final Term createPrimarykey(Artist artist) { |
||||||
Document doc = new Document(); |
return new Term(FieldNames.ID, Integer.toString(artist.getId())); |
||||||
doc.add(new NumericField(FieldNames.ID, Field.Store.YES, false) |
}; |
||||||
.setIntValue(artist.getId())); |
|
||||||
doc.add(new Field(FieldNames.ARTIST, artist.getName(), Field.Store.YES, |
public final Term createPrimarykey(MediaFile mediaFile) { |
||||||
Field.Index.ANALYZED)); |
return new Term(FieldNames.ID, Integer.toString(mediaFile.getId())); |
||||||
doc.add(new NumericField(FieldNames.FOLDER_ID, Field.Store.NO, true) |
}; |
||||||
.setIntValue(musicFolder.getId())); |
|
||||||
return doc; |
/** |
||||||
} |
* Create a document. |
||||||
|
* |
||||||
/** |
* @param mediaFile target of document |
||||||
* Create a document. |
* @return document |
||||||
* |
* @since legacy |
||||||
* @param mediaFile target of document |
*/ |
||||||
* @return document |
public Document createAlbumDocument(MediaFile mediaFile) { |
||||||
* @since legacy |
Document doc = new Document(); |
||||||
*/ |
fieldId.accept(doc, mediaFile.getId()); |
||||||
public Document createSongDocument(MediaFile mediaFile) { |
fieldWords.accept(doc, FieldNames.ARTIST, mediaFile.getArtist()); |
||||||
Document doc = new Document(); |
fieldWords.accept(doc, FieldNames.ALBUM, mediaFile.getAlbumName()); |
||||||
doc.add(new NumericField(FieldNames.ID, Field.Store.YES, false) |
fieldFolderPath.accept(doc, mediaFile.getFolder()); |
||||||
.setIntValue(mediaFile.getId())); |
return doc; |
||||||
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, |
* Create a document. |
||||||
Field.Index.ANALYZED)); |
* |
||||||
} |
* @param mediaFile target of document |
||||||
if (mediaFile.getArtist() != null) { |
* @return document |
||||||
doc.add(new Field(FieldNames.ARTIST, mediaFile.getArtist(), Field.Store.YES, |
* @since legacy |
||||||
Field.Index.ANALYZED)); |
*/ |
||||||
} |
public Document createArtistDocument(MediaFile mediaFile) { |
||||||
if (mediaFile.getGenre() != null) { |
Document doc = new Document(); |
||||||
doc.add(new Field(FieldNames.GENRE, normalizeGenre(mediaFile.getGenre()), |
fieldId.accept(doc, mediaFile.getId()); |
||||||
Field.Store.NO, Field.Index.ANALYZED)); |
fieldWords.accept(doc, FieldNames.ARTIST, mediaFile.getArtist()); |
||||||
} |
fieldFolderPath.accept(doc, mediaFile.getFolder()); |
||||||
if (mediaFile.getYear() != null) { |
return doc; |
||||||
doc.add(new NumericField(FieldNames.YEAR, Field.Store.NO, true) |
} |
||||||
.setIntValue(mediaFile.getYear())); |
|
||||||
} |
/** |
||||||
if (mediaFile.getFolder() != null) { |
* Create a document. |
||||||
doc.add(new Field(FieldNames.FOLDER, mediaFile.getFolder(), Field.Store.NO, |
* |
||||||
Field.Index.NOT_ANALYZED_NO_NORMS)); |
* @param album target of document |
||||||
} |
* @return document |
||||||
return doc; |
* @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. |
This file is part of Airsonic. |
||||||
|
|
||||||
Airsonic is free software: you can redistribute it and/or modify |
Airsonic is free software: you can redistribute it and/or modify |
||||||
it under the terms of the GNU General Public License as published by |
it under the terms of the GNU General Public License as published by |
||||||
the Free Software Foundation, either version 3 of the License, or |
the Free Software Foundation, either version 3 of the License, or |
||||||
(at your option) any later version. |
(at your option) any later version. |
||||||
|
|
||||||
Airsonic is distributed in the hope that it will be useful, |
Airsonic is distributed in the hope that it will be useful, |
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
GNU General Public License for more details. |
GNU General Public License for more details. |
||||||
|
|
||||||
You should have received a copy of the GNU General Public License |
You should have received a copy of the GNU General Public License |
||||||
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Copyright 2016 (C) Airsonic Authors |
Copyright 2016 (C) Airsonic Authors |
||||||
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus |
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus |
||||||
*/ |
*/ |
||||||
|
|
||||||
package org.airsonic.player.service.search; |
package org.airsonic.player.service.search; |
||||||
|
|
||||||
/** |
/** |
||||||
* Enum that symbolizes the field name used for lucene index. |
* 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. |
* This class is a division of what was once part of SearchService and added functionality. |
||||||
*/ |
*/ |
||||||
class FieldNames { |
class FieldNames { |
||||||
|
|
||||||
private FieldNames() { |
private FieldNames() { |
||||||
} |
} |
||||||
|
|
||||||
/** |
/** |
||||||
* A field same to a legacy server, id field. |
* A field same to a legacy server, id field. |
||||||
* |
* |
||||||
* @since legacy |
* @since legacy |
||||||
**/ |
**/ |
||||||
public static final String ID = "id"; |
public static final String ID = "id"; |
||||||
|
|
||||||
/** |
/** |
||||||
* A field same to a legacy server, id field. |
* A field same to a legacy server, id field. |
||||||
* |
* |
||||||
* @since legacy |
* @since legacy |
||||||
**/ |
**/ |
||||||
public static final String FOLDER_ID = "folderId"; |
public static final String FOLDER_ID = "folderId"; |
||||||
|
|
||||||
/** |
/** |
||||||
* A field same to a legacy server, numeric field. |
* A field same to a legacy server, numeric field. |
||||||
* |
* |
||||||
* @since legacy |
* @since legacy |
||||||
**/ |
**/ |
||||||
public static final String YEAR = "year"; |
public static final String YEAR = "year"; |
||||||
|
|
||||||
/** |
/** |
||||||
* A field same to a legacy server, key field. |
* A field same to a legacy server, key field. |
||||||
* |
* |
||||||
* @since legacy |
* @since legacy |
||||||
**/ |
**/ |
||||||
public static final String GENRE = "genre"; |
public static final String GENRE = "genre"; |
||||||
|
|
||||||
/** |
/** |
||||||
* A field same to a legacy server, key field. |
* A field same to a legacy server, key field. |
||||||
* |
* |
||||||
* @since legacy |
* @since legacy |
||||||
**/ |
**/ |
||||||
public static final String MEDIA_TYPE = "mediaType"; |
public static final String MEDIA_TYPE = "mediaType"; |
||||||
|
|
||||||
/** |
/** |
||||||
* A field same to a legacy server, key field. |
* A field same to a legacy server, key field. |
||||||
* |
* |
||||||
* @since legacy |
* @since legacy |
||||||
**/ |
**/ |
||||||
public static final String FOLDER = "folder"; |
public static final String FOLDER = "folder"; |
||||||
|
|
||||||
/** |
/** |
||||||
* A field same to a legacy server, usually with common word parsing. |
* A field same to a legacy server, usually with common word parsing. |
||||||
* |
* |
||||||
* @since legacy |
* @since legacy |
||||||
**/ |
**/ |
||||||
public static final String ARTIST = "artist"; |
public static final String ARTIST = "artist"; |
||||||
|
|
||||||
/** |
/** |
||||||
* A field same to a legacy server, usually with common word parsing. |
* A field same to a legacy server, usually with common word parsing. |
||||||
* |
* |
||||||
* @since legacy |
* @since legacy |
||||||
**/ |
**/ |
||||||
public static final String ALBUM = "album"; |
public static final String ALBUM = "album"; |
||||||
|
|
||||||
/** |
/** |
||||||
* A field same to a legacy server, usually with common word parsing. |
* A field same to a legacy server, usually with common word parsing. |
||||||
* |
* |
||||||
* @since legacy |
* @since legacy |
||||||
**/ |
**/ |
||||||
public static final String TITLE = "title"; |
public static final String TITLE = "title"; |
||||||
|
|
||||||
} |
} |
||||||
|
@ -1,169 +1,323 @@ |
|||||||
/* |
/* |
||||||
This file is part of Airsonic. |
This file is part of Airsonic. |
||||||
|
|
||||||
Airsonic is free software: you can redistribute it and/or modify |
Airsonic is free software: you can redistribute it and/or modify |
||||||
it under the terms of the GNU General Public License as published by |
it under the terms of the GNU General Public License as published by |
||||||
the Free Software Foundation, either version 3 of the License, or |
the Free Software Foundation, either version 3 of the License, or |
||||||
(at your option) any later version. |
(at your option) any later version. |
||||||
|
|
||||||
Airsonic is distributed in the hope that it will be useful, |
Airsonic is distributed in the hope that it will be useful, |
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
GNU General Public License for more details. |
GNU General Public License for more details. |
||||||
|
|
||||||
You should have received a copy of the GNU General Public License |
You should have received a copy of the GNU General Public License |
||||||
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Copyright 2016 (C) Airsonic Authors |
Copyright 2016 (C) Airsonic Authors |
||||||
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus |
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus |
||||||
*/ |
*/ |
||||||
|
|
||||||
package org.airsonic.player.service.search; |
package org.airsonic.player.service.search; |
||||||
|
|
||||||
import org.airsonic.player.domain.Album; |
import org.airsonic.player.domain.Album; |
||||||
import org.airsonic.player.domain.Artist; |
import org.airsonic.player.domain.Artist; |
||||||
import org.airsonic.player.domain.MediaFile; |
import org.airsonic.player.domain.MediaFile; |
||||||
import org.airsonic.player.domain.MusicFolder; |
import org.airsonic.player.domain.MusicFolder; |
||||||
import org.airsonic.player.service.SettingsService; |
import org.airsonic.player.service.SettingsService; |
||||||
import org.airsonic.player.util.FileUtil; |
import org.airsonic.player.util.FileUtil; |
||||||
import org.apache.lucene.index.IndexReader; |
import org.apache.commons.io.FileUtils; |
||||||
import org.apache.lucene.index.IndexWriter; |
import org.apache.lucene.document.Document; |
||||||
import org.apache.lucene.store.FSDirectory; |
import org.apache.lucene.index.IndexWriter; |
||||||
import org.slf4j.Logger; |
import org.apache.lucene.index.IndexWriterConfig; |
||||||
import org.slf4j.LoggerFactory; |
import org.apache.lucene.index.Term; |
||||||
import org.springframework.beans.factory.annotation.Autowired; |
import org.apache.lucene.search.IndexSearcher; |
||||||
import org.springframework.stereotype.Component; |
import org.apache.lucene.search.SearcherManager; |
||||||
|
import org.apache.lucene.store.FSDirectory; |
||||||
import java.io.File; |
import org.checkerframework.checker.nullness.qual.Nullable; |
||||||
import java.io.IOException; |
import org.slf4j.Logger; |
||||||
|
import org.slf4j.LoggerFactory; |
||||||
import static org.airsonic.player.service.search.IndexType.ALBUM; |
import org.springframework.beans.factory.annotation.Autowired; |
||||||
import static org.airsonic.player.service.search.IndexType.ALBUM_ID3; |
import org.springframework.stereotype.Component; |
||||||
import static org.airsonic.player.service.search.IndexType.ARTIST; |
|
||||||
import static org.airsonic.player.service.search.IndexType.ARTIST_ID3; |
import java.io.File; |
||||||
import static org.airsonic.player.service.search.IndexType.SONG; |
import java.io.IOException; |
||||||
|
import java.util.Arrays; |
||||||
/** |
import java.util.HashMap; |
||||||
* Function class that is strongly linked to the lucene index implementation. |
import java.util.Map; |
||||||
* Legacy has an implementation in SearchService. |
import java.util.function.Function; |
||||||
* |
import java.util.function.Supplier; |
||||||
* If the index CRUD and search functionality are in the same class, |
import java.util.regex.Pattern; |
||||||
* there is often a dependency conflict on the class used. |
|
||||||
* Although the interface of SearchService is left to maintain the legacy implementation, |
import static org.springframework.util.ObjectUtils.isEmpty; |
||||||
* it is desirable that methods of index operations other than search essentially use this class directly. |
|
||||||
*/ |
/** |
||||||
@Component |
* Function class that is strongly linked to the lucene index implementation. |
||||||
public class IndexManager { |
* Legacy has an implementation in SearchService. |
||||||
|
* |
||||||
private static final Logger LOG = LoggerFactory.getLogger(IndexManager.class); |
* If the index CRUD and search functionality are in the same class, |
||||||
|
* there is often a dependency conflict on the class used. |
||||||
@Autowired |
* Although the interface of SearchService is left to maintain the legacy implementation, |
||||||
private AnalyzerFactory analyzerFactory; |
* it is desirable that methods of index operations other than search essentially use this class directly. |
||||||
|
*/ |
||||||
@Autowired |
@Component |
||||||
private DocumentFactory documentFactory; |
public class IndexManager { |
||||||
|
|
||||||
private IndexWriter artistWriter; |
private static final Logger LOG = LoggerFactory.getLogger(IndexManager.class); |
||||||
private IndexWriter artistId3Writer; |
|
||||||
private IndexWriter albumWriter; |
/** |
||||||
private IndexWriter albumId3Writer; |
* Schema version of Airsonic index. |
||||||
private IndexWriter songWriter; |
* It may be incremented in the following cases: |
||||||
|
* |
||||||
public void index(Album album) { |
* - Incompatible update case in Lucene index implementation |
||||||
try { |
* - When schema definition is changed due to modification of AnalyzerFactory, |
||||||
albumId3Writer.addDocument(documentFactory.createAlbumId3Document(album)); |
* DocumentFactory or the class that they use. |
||||||
} catch (Exception x) { |
* |
||||||
LOG.error("Failed to create search index for " + album, x); |
*/ |
||||||
} |
private static final int INDEX_VERSION = 16; |
||||||
} |
|
||||||
|
/** |
||||||
public void index(Artist artist, MusicFolder musicFolder) { |
* Literal name of index top directory. |
||||||
try { |
*/ |
||||||
artistId3Writer |
private static final String INDEX_ROOT_DIR_NAME = "index"; |
||||||
.addDocument(documentFactory.createArtistId3Document(artist, musicFolder)); |
|
||||||
} catch (Exception x) { |
/** |
||||||
LOG.error("Failed to create search index for " + artist, x); |
* File supplier for index directory. |
||||||
} |
*/ |
||||||
} |
private Supplier<File> rootIndexDirectory = () -> |
||||||
|
new File(SettingsService.getAirsonicHome(), INDEX_ROOT_DIR_NAME.concat(Integer.toString(INDEX_VERSION))); |
||||||
public void index(MediaFile mediaFile) { |
|
||||||
try { |
/** |
||||||
if (mediaFile.isFile()) { |
* Returns the directory of the specified index |
||||||
songWriter.addDocument(documentFactory.createSongDocument(mediaFile)); |
*/ |
||||||
} else if (mediaFile.isAlbum()) { |
private Function<IndexType, File> getIndexDirectory = (indexType) -> |
||||||
albumWriter.addDocument(documentFactory.createAlbumDocument(mediaFile)); |
new File(rootIndexDirectory.get(), indexType.toString().toLowerCase()); |
||||||
} else { |
|
||||||
artistWriter.addDocument(documentFactory.createArtistDocument(mediaFile)); |
@Autowired |
||||||
} |
private AnalyzerFactory analyzerFactory; |
||||||
} catch (Exception x) { |
|
||||||
LOG.error("Failed to create search index for " + mediaFile, x); |
@Autowired |
||||||
} |
private DocumentFactory documentFactory; |
||||||
} |
|
||||||
|
private Map<IndexType, SearcherManager> searchers = new HashMap<>(); |
||||||
private static final String LUCENE_DIR = "lucene2"; |
|
||||||
|
private Map<IndexType, IndexWriter> writers = new HashMap<>(); |
||||||
public IndexReader createIndexReader(IndexType indexType) throws IOException { |
|
||||||
File dir = getIndexDirectory(indexType); |
public void index(Album album) { |
||||||
return IndexReader.open(FSDirectory.open(dir), true); |
Term primarykey = documentFactory.createPrimarykey(album); |
||||||
} |
Document document = documentFactory.createAlbumId3Document(album); |
||||||
|
try { |
||||||
/** |
writers.get(IndexType.ALBUM_ID3).updateDocument(primarykey, document); |
||||||
* It is static as an intermediate response of the transition period. |
} catch (Exception x) { |
||||||
* (It is called before injection because it is called by SearchService constructor) |
LOG.error("Failed to create search index for " + album, x); |
||||||
* |
} |
||||||
* @return |
} |
||||||
*/ |
|
||||||
private static File getIndexRootDirectory() { |
public void index(Artist artist, MusicFolder musicFolder) { |
||||||
return new File(SettingsService.getAirsonicHome(), LUCENE_DIR); |
Term primarykey = documentFactory.createPrimarykey(artist); |
||||||
} |
Document document = documentFactory.createArtistId3Document(artist, musicFolder); |
||||||
|
try { |
||||||
/** |
writers.get(IndexType.ARTIST_ID3).updateDocument(primarykey, document); |
||||||
* Make it public as an interim response of the transition period. |
} catch (Exception x) { |
||||||
* (It is called before the injection because it is called in the SearchService constructor.) |
LOG.error("Failed to create search index for " + artist, x); |
||||||
* |
} |
||||||
* @param indexType |
} |
||||||
* @return |
|
||||||
* @deprecated It should not be called from outside. |
public void index(MediaFile mediaFile) { |
||||||
*/ |
Term primarykey = documentFactory.createPrimarykey(mediaFile); |
||||||
@Deprecated |
try { |
||||||
public static File getIndexDirectory(IndexType indexType) { |
if (mediaFile.isFile()) { |
||||||
return new File(getIndexRootDirectory(), indexType.toString().toLowerCase()); |
Document document = documentFactory.createSongDocument(mediaFile); |
||||||
} |
writers.get(IndexType.SONG).updateDocument(primarykey, document); |
||||||
|
} else if (mediaFile.isAlbum()) { |
||||||
private IndexWriter createIndexWriter(IndexType indexType) throws IOException { |
Document document = documentFactory.createAlbumDocument(mediaFile); |
||||||
File dir = getIndexDirectory(indexType); |
writers.get(IndexType.ALBUM).updateDocument(primarykey, document); |
||||||
return new IndexWriter(FSDirectory.open(dir), analyzerFactory.getAnalyzer(), true, |
} else { |
||||||
new IndexWriter.MaxFieldLength(10)); |
Document document = documentFactory.createArtistDocument(mediaFile); |
||||||
} |
writers.get(IndexType.ARTIST).updateDocument(primarykey, document); |
||||||
|
} |
||||||
public final void startIndexing() { |
} catch (Exception x) { |
||||||
try { |
LOG.error("Failed to create search index for " + mediaFile, x); |
||||||
artistWriter = createIndexWriter(ARTIST); |
} |
||||||
artistId3Writer = createIndexWriter(ARTIST_ID3); |
} |
||||||
albumWriter = createIndexWriter(ALBUM); |
|
||||||
albumId3Writer = createIndexWriter(ALBUM_ID3); |
public final void startIndexing() { |
||||||
songWriter = createIndexWriter(SONG); |
try { |
||||||
} catch (Exception x) { |
for (IndexType IndexType : IndexType.values()) { |
||||||
LOG.error("Failed to create search index.", x); |
writers.put(IndexType, createIndexWriter(IndexType)); |
||||||
} |
} |
||||||
} |
} catch (IOException e) { |
||||||
|
LOG.error("Failed to create search index.", e); |
||||||
public void stopIndexing() { |
} |
||||||
try { |
} |
||||||
artistWriter.optimize(); |
|
||||||
artistId3Writer.optimize(); |
/** |
||||||
albumWriter.optimize(); |
* |
||||||
albumId3Writer.optimize(); |
* @param indexType |
||||||
songWriter.optimize(); |
* @return |
||||||
} catch (Exception x) { |
* @throws IOException |
||||||
LOG.error("Failed to create search index.", x); |
*/ |
||||||
} finally { |
private IndexWriter createIndexWriter(IndexType indexType) throws IOException { |
||||||
FileUtil.closeQuietly(artistId3Writer); |
File indexDirectory = getIndexDirectory.apply(indexType); |
||||||
FileUtil.closeQuietly(artistWriter); |
IndexWriterConfig config = new IndexWriterConfig(analyzerFactory.getAnalyzer()); |
||||||
FileUtil.closeQuietly(albumWriter); |
return new IndexWriter(FSDirectory.open(indexDirectory.toPath()), config); |
||||||
FileUtil.closeQuietly(albumId3Writer); |
} |
||||||
FileUtil.closeQuietly(songWriter); |
|
||||||
} |
/** |
||||||
} |
* 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. |
This file is part of Airsonic. |
||||||
|
|
||||||
Airsonic is free software: you can redistribute it and/or modify |
Airsonic is free software: you can redistribute it and/or modify |
||||||
it under the terms of the GNU General Public License as published by |
it under the terms of the GNU General Public License as published by |
||||||
the Free Software Foundation, either version 3 of the License, or |
the Free Software Foundation, either version 3 of the License, or |
||||||
(at your option) any later version. |
(at your option) any later version. |
||||||
|
|
||||||
Airsonic is distributed in the hope that it will be useful, |
Airsonic is distributed in the hope that it will be useful, |
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
GNU General Public License for more details. |
GNU General Public License for more details. |
||||||
|
|
||||||
You should have received a copy of the GNU General Public License |
You should have received a copy of the GNU General Public License |
||||||
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Copyright 2016 (C) Airsonic Authors |
Copyright 2016 (C) Airsonic Authors |
||||||
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus |
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus |
||||||
*/ |
*/ |
||||||
|
|
||||||
package org.airsonic.player.service.search; |
package org.airsonic.player.service.search; |
||||||
|
|
||||||
import org.airsonic.player.domain.MediaFile; |
import org.airsonic.player.domain.MediaFile.MediaType; |
||||||
import org.airsonic.player.domain.MusicFolder; |
import org.airsonic.player.domain.MusicFolder; |
||||||
import org.airsonic.player.domain.RandomSearchCriteria; |
import org.airsonic.player.domain.RandomSearchCriteria; |
||||||
import org.airsonic.player.domain.SearchCriteria; |
import org.airsonic.player.domain.SearchCriteria; |
||||||
import org.apache.lucene.analysis.ASCIIFoldingFilter; |
import org.apache.lucene.analysis.Analyzer; |
||||||
import org.apache.lucene.analysis.standard.StandardTokenizer; |
import org.apache.lucene.analysis.TokenStream; |
||||||
import org.apache.lucene.analysis.tokenattributes.TermAttribute; |
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; |
||||||
import org.apache.lucene.index.Term; |
import org.apache.lucene.document.IntPoint; |
||||||
import org.apache.lucene.queryParser.MultiFieldQueryParser; |
import org.apache.lucene.index.Term; |
||||||
import org.apache.lucene.queryParser.ParseException; |
import org.apache.lucene.search.BooleanClause.Occur; |
||||||
import org.apache.lucene.queryParser.QueryParser; |
import org.apache.lucene.search.BooleanQuery; |
||||||
import org.apache.lucene.search.BooleanClause; |
import org.apache.lucene.search.Query; |
||||||
import org.apache.lucene.search.BooleanQuery; |
import org.apache.lucene.search.TermQuery; |
||||||
import org.apache.lucene.search.NumericRangeQuery; |
import org.apache.lucene.search.WildcardQuery; |
||||||
import org.apache.lucene.search.Query; |
import org.checkerframework.checker.nullness.qual.NonNull; |
||||||
import org.apache.lucene.search.TermQuery; |
import org.checkerframework.checker.nullness.qual.Nullable; |
||||||
import org.apache.lucene.search.spans.SpanOrQuery; |
import org.springframework.beans.factory.annotation.Autowired; |
||||||
import org.apache.lucene.search.spans.SpanQuery; |
import org.springframework.stereotype.Component; |
||||||
import org.apache.lucene.search.spans.SpanTermQuery; |
|
||||||
import org.apache.lucene.util.NumericUtils; |
import java.io.IOException; |
||||||
import org.apache.lucene.util.Version; |
import java.util.ArrayList; |
||||||
import org.springframework.beans.factory.annotation.Autowired; |
import java.util.List; |
||||||
import org.springframework.stereotype.Component; |
import java.util.function.BiFunction; |
||||||
|
import java.util.function.Function; |
||||||
import java.io.IOException; |
|
||||||
import java.io.StringReader; |
import static org.springframework.util.ObjectUtils.isEmpty; |
||||||
import java.util.ArrayList; |
|
||||||
import java.util.List; |
/** |
||||||
|
* Factory class of Lucene Query. |
||||||
import static org.airsonic.player.service.search.IndexType.ALBUM_ID3; |
* This class is an extract of the functionality that was once part of SearchService. |
||||||
import static org.airsonic.player.service.search.IndexType.ARTIST_ID3; |
* It is for maintainability and verification. |
||||||
|
* Each corresponds to the SearchService method. |
||||||
/** |
* The API syntax for query generation depends on the lucene version. |
||||||
* Factory class of Lucene Query. |
* verification with query grammar is possible. |
||||||
* This class is an extract of the functionality that was once part of SearchService. |
* On the other hand, the generated queries are relatively small by version. |
||||||
* It is for maintainability and verification. |
* Therefore, test cases of this class are useful for large version upgrades. |
||||||
* Each corresponds to the SearchService method. |
**/ |
||||||
* The API syntax for query generation depends on the lucene version. |
@Component |
||||||
* verification with query grammar is possible. |
public class QueryFactory { |
||||||
* On the other hand, the generated queries are relatively small by version. |
|
||||||
* Therefore, test cases of this class are useful for large version upgrades. |
private static final String ASTERISK = "*"; |
||||||
**/ |
|
||||||
@Component |
@Autowired |
||||||
public class QueryFactory { |
private AnalyzerFactory analyzerFactory; |
||||||
|
|
||||||
@Autowired |
private final Function<MusicFolder, Query> toFolderIdQuery = (folder) -> { |
||||||
private AnalyzerFactory analyzerFactory; |
// Unanalyzed field
|
||||||
|
return new TermQuery(new Term(FieldNames.FOLDER_ID, folder.getId().toString())); |
||||||
private String analyzeQuery(String query) throws IOException { |
}; |
||||||
StringBuilder result = new StringBuilder(); |
|
||||||
/* |
private final Function<MusicFolder, Query> toFolderPathQuery = (folder) -> { |
||||||
* Version.LUCENE_30 |
// Unanalyzed field
|
||||||
* It is a transient description and will be deleted when upgrading the version. |
return new TermQuery(new Term(FieldNames.FOLDER, folder.getPath().getPath())); |
||||||
* SearchService variables are not used because the reference direction conflicts. |
}; |
||||||
*/ |
|
||||||
@SuppressWarnings("resource") |
/* |
||||||
ASCIIFoldingFilter filter = new ASCIIFoldingFilter( |
* XXX 3.x -> 8.x : |
||||||
new StandardTokenizer(Version.LUCENE_30, new StringReader(query))); |
* "SpanOr" has been changed to "Or". |
||||||
TermAttribute termAttribute = filter.getAttribute(TermAttribute.class); |
* - Path comparison is more appropriate with "Or". |
||||||
while (filter.incrementToken()) { |
* - If "SpanOr" is maintained, the DOC design needs to be changed. |
||||||
result.append(termAttribute.term()).append("* "); |
*/ |
||||||
} |
private final BiFunction<@NonNull Boolean, @NonNull List<MusicFolder>, @NonNull Query> toFolderQuery = ( |
||||||
return result.toString(); |
isId3, folders) -> { |
||||||
} |
BooleanQuery.Builder mfQuery = new BooleanQuery.Builder(); |
||||||
|
folders.stream() |
||||||
/** |
.map(isId3 ? toFolderIdQuery : toFolderPathQuery) |
||||||
* Normalize the genre string. |
.forEach(t -> mfQuery.add(t, Occur.SHOULD)); |
||||||
* |
return mfQuery.build(); |
||||||
* @param genre genre string |
}; |
||||||
* @return genre string normalized |
|
||||||
* @deprecated should be resolved with tokenizer or filter |
/* |
||||||
*/ |
* XXX 3.x -> 8.x : |
||||||
@Deprecated |
* In order to support wildcards, |
||||||
private String normalizeGenre(String genre) { |
* MultiFieldQueryParser has been replaced by the following process. |
||||||
return genre.toLowerCase().replace(" ", "").replace("-", ""); |
* |
||||||
} |
* - 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. |
||||||
* Query generation expression extracted from |
* - Self made parser process reduces one library dependency. |
||||||
* {@link org.airsonic.player.service.SearchService#search(SearchCriteria, List, IndexType)}. |
* - It is easy to make corrections later when changing the query to improve search accuracy. |
||||||
* |
*/ |
||||||
* @param criteria criteria |
private Query createMultiFieldWildQuery(@NonNull String[] fieldNames, @NonNull String queryString) |
||||||
* @param musicFolders musicFolders |
throws IOException { |
||||||
* @param indexType {@link IndexType} |
|
||||||
* @return Query |
BooleanQuery.Builder mainQuery = new BooleanQuery.Builder(); |
||||||
* @throws IOException When parsing of MultiFieldQueryParser fails |
|
||||||
* @throws ParseException When parsing of MultiFieldQueryParser fails |
List<List<Query>> fieldsQuerys = new ArrayList<>(); |
||||||
*/ |
Analyzer analyzer = analyzerFactory.getQueryAnalyzer(); |
||||||
public Query search(SearchCriteria criteria, List<MusicFolder> musicFolders, |
|
||||||
IndexType indexType) throws ParseException, IOException { |
/* Wildcard applies to all tokens. **/ |
||||||
/* |
for (String fieldName : fieldNames) { |
||||||
* Version.LUCENE_30 |
try (TokenStream stream = analyzer.tokenStream(fieldName, queryString)) { |
||||||
* It is a transient description and will be deleted when upgrading the version. |
stream.reset(); |
||||||
* SearchService variables are not used because the reference direction conflicts. |
List<Query> fieldQuerys = new ArrayList<>(); |
||||||
*/ |
while (stream.incrementToken()) { |
||||||
MultiFieldQueryParser queryParser = new MultiFieldQueryParser(Version.LUCENE_30, |
String token = stream.getAttribute(CharTermAttribute.class).toString(); |
||||||
indexType.getFields(), analyzerFactory.getQueryAnalyzer(), indexType.getBoosts()); |
WildcardQuery wildcardQuery = new WildcardQuery(new Term(fieldName, token.concat(ASTERISK))); |
||||||
|
fieldQuerys.add(wildcardQuery); |
||||||
BooleanQuery query = new BooleanQuery(); |
} |
||||||
query.add(queryParser.parse(analyzeQuery(criteria.getQuery())), BooleanClause.Occur.MUST); |
fieldsQuerys.add(fieldQuerys); |
||||||
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, |
/* If Field's Tokenizer is different, token's length may not match. **/ |
||||||
NumericUtils.intToPrefixCoded(musicFolder.getId())))); |
int maxTermLength = fieldsQuerys.stream() |
||||||
} else { |
.map(l -> l.size()) |
||||||
musicFolderQueries.add(new SpanTermQuery( |
.max(Integer::compare).get(); |
||||||
new Term(FieldNames.FOLDER, musicFolder.getPath().getPath()))); |
|
||||||
} |
if (0 < fieldsQuerys.size()) { |
||||||
} |
for (int i = 0; i < maxTermLength; i++) { |
||||||
query.add( |
BooleanQuery.Builder fieldsQuery = new BooleanQuery.Builder(); |
||||||
new SpanOrQuery( |
for (List<Query> fieldQuerys : fieldsQuerys) { |
||||||
musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])), |
if (i < fieldQuerys.size()) { |
||||||
BooleanClause.Occur.MUST); |
fieldsQuery.add(fieldQuerys.get(i), Occur.SHOULD); |
||||||
return query; |
} |
||||||
} |
} |
||||||
|
mainQuery.add(fieldsQuery.build(), Occur.SHOULD); |
||||||
/** |
} |
||||||
* Query generation expression extracted from |
} |
||||||
* {@link org.airsonic.player.service.SearchService#getRandomSongs(RandomSearchCriteria)}. |
|
||||||
* |
return mainQuery.build(); |
||||||
* @param criteria criteria |
|
||||||
* @return Query |
}; |
||||||
*/ |
|
||||||
public Query getRandomSongs(RandomSearchCriteria criteria) { |
/* |
||||||
BooleanQuery query = new BooleanQuery(); |
* XXX 3.x -> 8.x : |
||||||
query.add(new TermQuery( |
* RangeQuery has been changed to not allow null. |
||||||
new Term(FieldNames.MEDIA_TYPE, MediaFile.MediaType.MUSIC.name().toLowerCase())), |
*/ |
||||||
BooleanClause.Occur.MUST); |
private final BiFunction<@Nullable Integer, @Nullable Integer, @NonNull Query> toYearRangeQuery = |
||||||
if (criteria.getGenre() != null) { |
(from, to) -> { |
||||||
String genre = normalizeGenre(criteria.getGenre()); |
return IntPoint.newRangeQuery(FieldNames.YEAR, |
||||||
query.add(new TermQuery(new Term(FieldNames.GENRE, genre)), BooleanClause.Occur.MUST); |
isEmpty(from) ? Integer.MIN_VALUE : from, |
||||||
} |
isEmpty(to) ? Integer.MAX_VALUE : to); |
||||||
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); |
* Query generation expression extracted from |
||||||
} |
* {@link org.airsonic.player.service.SearchService#search(SearchCriteria, List, IndexType)}. |
||||||
|
* |
||||||
List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>(); |
* @param criteria criteria |
||||||
for (MusicFolder musicFolder : criteria.getMusicFolders()) { |
* @param musicFolders musicFolders |
||||||
musicFolderQueries.add(new SpanTermQuery( |
* @param indexType {@link IndexType} |
||||||
new Term(FieldNames.FOLDER, musicFolder.getPath().getPath()))); |
* @return Query |
||||||
} |
* @throws IOException When parsing of MultiFieldQueryParser fails |
||||||
query.add( |
*/ |
||||||
new SpanOrQuery( |
public Query search(SearchCriteria criteria, List<MusicFolder> musicFolders, |
||||||
musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])), |
IndexType indexType) throws IOException { |
||||||
BooleanClause.Occur.MUST); |
|
||||||
return query; |
BooleanQuery.Builder mainQuery = new BooleanQuery.Builder(); |
||||||
} |
|
||||||
|
Query multiFieldQuery = createMultiFieldWildQuery(indexType.getFields(), criteria.getQuery()); |
||||||
/** |
mainQuery.add(multiFieldQuery, Occur.MUST); |
||||||
* Query generation expression extracted from |
|
||||||
* {@link org.airsonic.player.service.SearchService#searchByName( String, String, int, int, List, Class)}. |
boolean isId3 = indexType == IndexType.ALBUM_ID3 || indexType == IndexType.ARTIST_ID3; |
||||||
* |
Query folderQuery = toFolderQuery.apply(isId3, musicFolders); |
||||||
* @param fieldName {@link FieldNames} |
mainQuery.add(folderQuery, Occur.MUST); |
||||||
* @return Query |
|
||||||
* @throws ParseException When parsing of QueryParser fails |
return mainQuery.build(); |
||||||
*/ |
|
||||||
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. |
* Query generation expression extracted from |
||||||
* SearchService variables are not used because the reference direction conflicts. |
* {@link org.airsonic.player.service.SearchService#getRandomSongs(RandomSearchCriteria)}. |
||||||
*/ |
* |
||||||
QueryParser queryParser = new QueryParser(Version.LUCENE_30, fieldName, |
* @param criteria criteria |
||||||
analyzerFactory.getQueryAnalyzer()); |
* @return Query |
||||||
Query query = queryParser.parse(name + "*"); |
* @throws IOException |
||||||
return query; |
*/ |
||||||
} |
public Query getRandomSongs(RandomSearchCriteria criteria) throws IOException { |
||||||
|
|
||||||
/** |
BooleanQuery.Builder query = new BooleanQuery.Builder(); |
||||||
* Query generation expression extracted from |
|
||||||
* {@link org.airsonic.player.service.SearchService#getRandomAlbums(int, List)}. |
Analyzer analyzer = analyzerFactory.getQueryAnalyzer(); |
||||||
* |
|
||||||
* @param musicFolders musicFolders |
// Unanalyzed field
|
||||||
* @return Query |
query.add(new TermQuery(new Term(FieldNames.MEDIA_TYPE, MediaType.MUSIC.name())), Occur.MUST); |
||||||
*/ |
|
||||||
public Query getRandomAlbums(List<MusicFolder> musicFolders) { |
if (!isEmpty(criteria.getGenre())) { |
||||||
List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>(); |
|
||||||
for (MusicFolder musicFolder : musicFolders) { |
// Unanalyzed field, but performs filtering according to id3 tag parser.
|
||||||
musicFolderQueries.add(new SpanTermQuery( |
try (TokenStream stream = analyzer.tokenStream(FieldNames.GENRE, criteria.getGenre())) { |
||||||
new Term(FieldNames.FOLDER, musicFolder.getPath().getPath()))); |
stream.reset(); |
||||||
} |
if (stream.incrementToken()) { |
||||||
Query query = new SpanOrQuery( |
String token = stream.getAttribute(CharTermAttribute.class).toString(); |
||||||
musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])); |
query.add(new TermQuery(new Term(FieldNames.GENRE, token)), Occur.MUST); |
||||||
return query; |
} |
||||||
} |
} |
||||||
|
} |
||||||
/** |
|
||||||
* Query generation expression extracted from |
if (!(isEmpty(criteria.getFromYear()) && isEmpty(criteria.getToYear()))) { |
||||||
* {@link org.airsonic.player.service.SearchService#getRandomAlbumsId3(int, List)}. |
query.add(toYearRangeQuery.apply(criteria.getFromYear(), criteria.getToYear()), Occur.MUST); |
||||||
* |
} |
||||||
* @param musicFolders musicFolders |
|
||||||
* @return Query |
query.add(toFolderQuery.apply(false, criteria.getMusicFolders()), Occur.MUST); |
||||||
*/ |
|
||||||
public Query getRandomAlbumsId3(List<MusicFolder> musicFolders) { |
return query.build(); |
||||||
|
|
||||||
List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>(); |
} |
||||||
for (MusicFolder musicFolder : musicFolders) { |
|
||||||
musicFolderQueries.add(new SpanTermQuery(new Term(FieldNames.FOLDER_ID, |
/** |
||||||
NumericUtils.intToPrefixCoded(musicFolder.getId())))); |
* Query generation expression extracted from |
||||||
} |
* {@link org.airsonic.player.service.SearchService#searchByName( String, String, int, int, List, Class)}. |
||||||
Query query = new SpanOrQuery( |
* |
||||||
musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])); |
* @param fieldName {@link FieldNames} |
||||||
return query; |
* @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. |
This file is part of Airsonic. |
||||||
|
|
||||||
Airsonic is free software: you can redistribute it and/or modify |
Airsonic is free software: you can redistribute it and/or modify |
||||||
it under the terms of the GNU General Public License as published by |
it under the terms of the GNU General Public License as published by |
||||||
the Free Software Foundation, either version 3 of the License, or |
the Free Software Foundation, either version 3 of the License, or |
||||||
(at your option) any later version. |
(at your option) any later version. |
||||||
|
|
||||||
Airsonic is distributed in the hope that it will be useful, |
Airsonic is distributed in the hope that it will be useful, |
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
GNU General Public License for more details. |
GNU General Public License for more details. |
||||||
|
|
||||||
You should have received a copy of the GNU General Public License |
You should have received a copy of the GNU General Public License |
||||||
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Copyright 2016 (C) Airsonic Authors |
Copyright 2016 (C) Airsonic Authors |
||||||
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus |
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus |
||||||
*/ |
*/ |
||||||
|
|
||||||
package org.airsonic.player.service.search; |
package org.airsonic.player.service.search; |
||||||
|
|
||||||
import org.airsonic.player.dao.AlbumDao; |
import org.airsonic.player.dao.AlbumDao; |
||||||
import org.airsonic.player.dao.ArtistDao; |
import org.airsonic.player.dao.ArtistDao; |
||||||
import org.airsonic.player.domain.Album; |
import org.airsonic.player.domain.Album; |
||||||
import org.airsonic.player.domain.Artist; |
import org.airsonic.player.domain.Artist; |
||||||
import org.airsonic.player.domain.MediaFile; |
import org.airsonic.player.domain.MediaFile; |
||||||
import org.airsonic.player.domain.ParamSearchResult; |
import org.airsonic.player.domain.ParamSearchResult; |
||||||
import org.airsonic.player.domain.SearchResult; |
import org.airsonic.player.domain.SearchResult; |
||||||
import org.airsonic.player.service.MediaFileService; |
import org.airsonic.player.service.MediaFileService; |
||||||
import org.airsonic.player.service.SettingsService; |
import org.apache.commons.collections.CollectionUtils; |
||||||
import org.apache.commons.collections.CollectionUtils; |
import org.apache.lucene.document.Document; |
||||||
import org.apache.lucene.document.Document; |
import org.checkerframework.checker.nullness.qual.Nullable; |
||||||
import org.apache.lucene.index.Term; |
import org.springframework.beans.factory.annotation.Autowired; |
||||||
import org.checkerframework.checker.nullness.qual.Nullable; |
import org.springframework.stereotype.Component; |
||||||
import org.springframework.beans.factory.annotation.Autowired; |
|
||||||
import org.springframework.stereotype.Component; |
import java.util.Collection; |
||||||
|
import java.util.List; |
||||||
import java.io.File; |
import java.util.function.BiConsumer; |
||||||
import java.util.Collection; |
import java.util.function.Function; |
||||||
import java.util.List; |
|
||||||
import java.util.function.BiConsumer; |
import static org.springframework.util.ObjectUtils.isEmpty; |
||||||
import java.util.function.BiFunction; |
|
||||||
import java.util.function.Function; |
/** |
||||||
|
* Termination used by SearchService. |
||||||
import static org.springframework.util.ObjectUtils.isEmpty; |
* |
||||||
|
* Since SearchService operates as a proxy for storage (DB) using lucene, |
||||||
/** |
* there are many redundant descriptions different from essential data processing. |
||||||
* Termination used by SearchService. |
* This class is a transfer class for saving those redundant descriptions. |
||||||
* |
* |
||||||
* Since SearchService operates as a proxy for storage (DB) using lucene, |
* Exception handling is not termination, |
||||||
* there are many redundant descriptions different from essential data processing. |
* so do not include exception handling in this class. |
||||||
* This class is a transfer class for saving those redundant descriptions. |
*/ |
||||||
* |
@Component |
||||||
* Exception handling is not termination, |
public class SearchServiceUtilities { |
||||||
* so do not include exception handling in this class. |
|
||||||
*/ |
/* Search by id only. */ |
||||||
@Component |
@Autowired |
||||||
public class SearchServiceUtilities { |
private ArtistDao artistDao; |
||||||
|
|
||||||
/* Search by id only. */ |
/* Search by id only. */ |
||||||
@Autowired |
@Autowired |
||||||
private ArtistDao artistDao; |
private AlbumDao albumDao; |
||||||
|
|
||||||
/* Search by id only. */ |
/* |
||||||
@Autowired |
* Search by id only. |
||||||
private AlbumDao albumDao; |
* 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. |
||||||
* Search by id only. |
*/ |
||||||
* Although there is no influence at present, |
@Autowired |
||||||
* mediaFileService has a caching mechanism. |
private MediaFileService mediaFileService; |
||||||
* Service is used instead of Dao until you are sure you need to use mediaFileDao. |
|
||||||
*/ |
public final Function<Long, Integer> round = (i) -> { |
||||||
@Autowired |
// return
|
||||||
private MediaFileService mediaFileService; |
// NumericUtils.floatToSortableInt(i);
|
||||||
|
return i.intValue(); |
||||||
public final Function<Long, Integer> round = (i) -> { |
}; |
||||||
// return
|
|
||||||
// NumericUtils.floatToSortableInt(i);
|
public final Function<Document, Integer> getId = d -> { |
||||||
return i.intValue(); |
return Integer.valueOf(d.get(FieldNames.ID)); |
||||||
}; |
}; |
||||||
|
|
||||||
public final Function<Document, Integer> getId = d -> { |
public final BiConsumer<List<MediaFile>, Integer> addMediaFileIfAnyMatch = (dist, id) -> { |
||||||
return Integer.valueOf(d.get(FieldNames.ID)); |
if (!dist.stream().anyMatch(m -> id == m.getId())) { |
||||||
}; |
MediaFile mediaFile = mediaFileService.getMediaFile(id); |
||||||
|
if (!isEmpty(mediaFile)) { |
||||||
public final BiConsumer<List<MediaFile>, Integer> addMediaFileIfAnyMatch = (dist, id) -> { |
dist.add(mediaFile); |
||||||
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)) { |
||||||
public final BiConsumer<List<Artist>, Integer> addArtistId3IfAnyMatch = (dist, id) -> { |
dist.add(artist); |
||||||
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; |
||||||
public final Function<Class<?>, @Nullable IndexType> getIndexType = (assignableClass) -> { |
} else if (assignableClass.isAssignableFrom(Artist.class)) { |
||||||
IndexType indexType = null; |
indexType = IndexType.ARTIST_ID3; |
||||||
if (assignableClass.isAssignableFrom(Album.class)) { |
} else if (assignableClass.isAssignableFrom(MediaFile.class)) { |
||||||
indexType = IndexType.ALBUM_ID3; |
indexType = IndexType.SONG; |
||||||
} else if (assignableClass.isAssignableFrom(Artist.class)) { |
} |
||||||
indexType = IndexType.ARTIST_ID3; |
return indexType; |
||||||
} else if (assignableClass.isAssignableFrom(MediaFile.class)) { |
}; |
||||||
indexType = IndexType.SONG; |
|
||||||
} |
public final Function<Class<?>, @Nullable String> getFieldName = (assignableClass) -> { |
||||||
return indexType; |
String fieldName = null; |
||||||
}; |
if (assignableClass.isAssignableFrom(Album.class)) { |
||||||
|
fieldName = FieldNames.ALBUM; |
||||||
public final Function<Class<?>, @Nullable String> getFieldName = (assignableClass) -> { |
} else if (assignableClass.isAssignableFrom(Artist.class)) { |
||||||
String fieldName = null; |
fieldName = FieldNames.ARTIST; |
||||||
if (assignableClass.isAssignableFrom(Album.class)) { |
} else if (assignableClass.isAssignableFrom(MediaFile.class)) { |
||||||
fieldName = FieldNames.ALBUM; |
fieldName = FieldNames.TITLE; |
||||||
} else if (assignableClass.isAssignableFrom(Artist.class)) { |
} |
||||||
fieldName = FieldNames.ARTIST; |
return fieldName; |
||||||
} else if (assignableClass.isAssignableFrom(MediaFile.class)) { |
}; |
||||||
fieldName = FieldNames.TITLE; |
|
||||||
} |
public final BiConsumer<List<Album>, Integer> addAlbumId3IfAnyMatch = (dist, subjectId) -> { |
||||||
return fieldName; |
if (!dist.stream().anyMatch(a -> subjectId == a.getId())) { |
||||||
}; |
Album album = albumDao.getAlbum(subjectId); |
||||||
|
if (!isEmpty(album)) { |
||||||
public final BiConsumer<List<Album>, Integer> addAlbumId3IfAnyMatch = (dist, subjectId) -> { |
dist.add(album); |
||||||
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); |
||||||
}; |
} |
||||||
|
|
||||||
private final Function<String, File> getRootDirectory = (version) -> { |
public final boolean addIgnoreNull(Collection<?> collection, IndexType indexType, |
||||||
return new File(SettingsService.getAirsonicHome(), version); |
int subjectId) { |
||||||
}; |
if (indexType == IndexType.ALBUM | indexType == IndexType.SONG) { |
||||||
|
return addIgnoreNull(collection, mediaFileService.getMediaFile(subjectId)); |
||||||
public final BiFunction<String, IndexType, File> getDirectory = (version, indexType) -> { |
} else if (indexType == IndexType.ALBUM_ID3) { |
||||||
return new File(getRootDirectory.apply(version), indexType.toString().toLowerCase()); |
return addIgnoreNull(collection, albumDao.getAlbum(subjectId)); |
||||||
}; |
} |
||||||
|
return false; |
||||||
public final Term createPrimarykey(Album album) { |
} |
||||||
return new Term(FieldNames.ID, Integer.toString(album.getId())); |
|
||||||
}; |
public final <T> void addIgnoreNull(ParamSearchResult<T> dist, IndexType indexType, |
||||||
|
int subjectId, Class<T> subjectClass) { |
||||||
public final Term createPrimarykey(Artist artist) { |
if (indexType == IndexType.SONG) { |
||||||
return new Term(FieldNames.ID, Integer.toString(artist.getId())); |
MediaFile mediaFile = mediaFileService.getMediaFile(subjectId); |
||||||
}; |
addIgnoreNull(dist.getItems(), subjectClass.cast(mediaFile)); |
||||||
|
} else if (indexType == IndexType.ARTIST_ID3) { |
||||||
public final Term createPrimarykey(MediaFile mediaFile) { |
Artist artist = artistDao.getArtist(subjectId); |
||||||
return new Term(FieldNames.ID, Integer.toString(mediaFile.getId())); |
addIgnoreNull(dist.getItems(), subjectClass.cast(artist)); |
||||||
}; |
} else if (indexType == IndexType.ALBUM_ID3) { |
||||||
|
Album album = albumDao.getAlbum(subjectId); |
||||||
public final boolean addIgnoreNull(Collection<?> collection, Object object) { |
addIgnoreNull(dist.getItems(), subjectClass.cast(album)); |
||||||
return CollectionUtils.addIgnoreNull(collection, object); |
} |
||||||
} |
} |
||||||
|
|
||||||
public final boolean addIgnoreNull(Collection<?> collection, IndexType indexType, |
public final void addIfAnyMatch(SearchResult dist, IndexType subjectIndexType, |
||||||
int subjectId) { |
Document subject) { |
||||||
if (indexType == IndexType.ALBUM | indexType == IndexType.SONG) { |
int documentId = getId.apply(subject); |
||||||
return addIgnoreNull(collection, mediaFileService.getMediaFile(subjectId)); |
if (subjectIndexType == IndexType.ARTIST | subjectIndexType == IndexType.ALBUM |
||||||
} else if (indexType == IndexType.ALBUM_ID3) { |
| subjectIndexType == IndexType.SONG) { |
||||||
return addIgnoreNull(collection, albumDao.getAlbum(subjectId)); |
addMediaFileIfAnyMatch.accept(dist.getMediaFiles(), documentId); |
||||||
} |
} else if (subjectIndexType == IndexType.ARTIST_ID3) { |
||||||
return false; |
addArtistId3IfAnyMatch.accept(dist.getArtists(), documentId); |
||||||
} |
} else if (subjectIndexType == IndexType.ALBUM_ID3) { |
||||||
|
addAlbumId3IfAnyMatch.accept(dist.getAlbums(), documentId); |
||||||
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; |
package org.airsonic.player.service.search; |
||||||
|
|
||||||
import java.util.Map; |
import java.util.Map; |
||||||
import java.util.concurrent.atomic.AtomicBoolean; |
import java.util.concurrent.atomic.AtomicBoolean; |
||||||
import java.util.function.Function; |
import java.util.function.Function; |
||||||
|
|
||||||
import org.airsonic.player.TestCaseUtils; |
import org.airsonic.player.TestCaseUtils; |
||||||
import org.airsonic.player.dao.DaoHelper; |
import org.airsonic.player.dao.DaoHelper; |
||||||
import org.airsonic.player.dao.MusicFolderDao; |
import org.airsonic.player.dao.MusicFolderDao; |
||||||
import org.airsonic.player.service.MediaScannerService; |
import org.airsonic.player.service.MediaScannerService; |
||||||
import org.airsonic.player.service.SettingsService; |
import org.airsonic.player.service.SettingsService; |
||||||
import org.airsonic.player.util.HomeRule; |
import org.airsonic.player.util.HomeRule; |
||||||
import org.airsonic.player.util.MusicFolderTestData; |
import org.airsonic.player.util.MusicFolderTestData; |
||||||
import org.junit.ClassRule; |
import org.junit.ClassRule; |
||||||
import org.junit.Rule; |
import org.junit.Rule; |
||||||
import org.junit.rules.TemporaryFolder; |
import org.junit.rules.TemporaryFolder; |
||||||
import org.junit.runner.Description; |
import org.junit.runner.Description; |
||||||
import org.junit.runners.model.Statement; |
import org.junit.runners.model.Statement; |
||||||
import org.springframework.beans.factory.annotation.Autowired; |
import org.springframework.beans.factory.annotation.Autowired; |
||||||
import org.springframework.stereotype.Component; |
import org.springframework.stereotype.Component; |
||||||
import org.springframework.test.annotation.DirtiesContext; |
import org.springframework.test.annotation.DirtiesContext; |
||||||
import org.springframework.test.context.ContextConfiguration; |
import org.springframework.test.context.ContextConfiguration; |
||||||
import org.springframework.test.context.junit4.rules.SpringClassRule; |
import org.springframework.test.context.junit4.rules.SpringClassRule; |
||||||
import org.springframework.test.context.junit4.rules.SpringMethodRule; |
import org.springframework.test.context.junit4.rules.SpringMethodRule; |
||||||
|
|
||||||
@ContextConfiguration(locations = { |
@ContextConfiguration(locations = { |
||||||
"/applicationContext-service.xml", |
"/applicationContext-service.xml", |
||||||
"/applicationContext-cache.xml", |
"/applicationContext-cache.xml", |
||||||
"/applicationContext-testdb.xml"}) |
"/applicationContext-testdb.xml"}) |
||||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) |
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) |
||||||
@Component |
@Component |
||||||
/** |
/** |
||||||
* Abstract class for scanning MusicFolder. |
* Abstract class for scanning MusicFolder. |
||||||
*/ |
*/ |
||||||
public abstract class AbstractAirsonicHomeTest implements AirsonicHomeTest { |
public abstract class AbstractAirsonicHomeTest implements AirsonicHomeTest { |
||||||
|
|
||||||
@ClassRule |
@ClassRule |
||||||
public static final SpringClassRule classRule = new SpringClassRule() { |
public static final SpringClassRule classRule = new SpringClassRule() { |
||||||
HomeRule homeRule = new HomeRule(); |
HomeRule homeRule = new HomeRule(); |
||||||
|
|
||||||
@Override |
@Override |
||||||
public Statement apply(Statement base, Description description) { |
public Statement apply(Statement base, Description description) { |
||||||
Statement spring = super.apply(base, description); |
Statement spring = super.apply(base, description); |
||||||
return homeRule.apply(spring, description); |
return homeRule.apply(spring, description); |
||||||
} |
} |
||||||
}; |
}; |
||||||
|
|
||||||
/* |
/* |
||||||
* Currently, Maven is executing test classes in series, |
* Currently, Maven is executing test classes in series, |
||||||
* so this class can hold the state. |
* so this class can hold the state. |
||||||
* When executing in parallel, subclasses should override this. |
* When executing in parallel, subclasses should override this. |
||||||
*/ |
*/ |
||||||
private static AtomicBoolean dataBasePopulated = new AtomicBoolean(); |
private static AtomicBoolean dataBasePopulated = new AtomicBoolean(); |
||||||
|
|
||||||
// Above.
|
// Above.
|
||||||
private static AtomicBoolean dataBaseReady = new AtomicBoolean(); |
private static AtomicBoolean dataBaseReady = new AtomicBoolean(); |
||||||
|
|
||||||
protected final static Function<String, String> resolveBaseMediaPath = (childPath) -> { |
protected final static Function<String, String> resolveBaseMediaPath = (childPath) -> { |
||||||
return MusicFolderTestData.resolveBaseMediaPath().concat(childPath); |
return MusicFolderTestData.resolveBaseMediaPath().concat(childPath); |
||||||
}; |
}; |
||||||
|
|
||||||
@Autowired |
@Autowired |
||||||
protected DaoHelper daoHelper; |
protected DaoHelper daoHelper; |
||||||
|
|
||||||
@Autowired |
@Autowired |
||||||
protected MediaScannerService mediaScannerService; |
protected MediaScannerService mediaScannerService; |
||||||
|
|
||||||
@Autowired |
@Autowired |
||||||
protected MusicFolderDao musicFolderDao; |
protected MusicFolderDao musicFolderDao; |
||||||
|
|
||||||
@Autowired |
@Autowired |
||||||
protected SettingsService settingsService; |
protected SettingsService settingsService; |
||||||
|
|
||||||
@Rule |
@Rule |
||||||
public final SpringMethodRule springMethodRule = new SpringMethodRule(); |
public final SpringMethodRule springMethodRule = new SpringMethodRule(); |
||||||
|
|
||||||
@Rule |
@Rule |
||||||
public TemporaryFolder temporaryFolder = new TemporaryFolder(); |
public TemporaryFolder temporaryFolder = new TemporaryFolder(); |
||||||
|
|
||||||
@Override |
@Override |
||||||
public AtomicBoolean dataBasePopulated() { |
public AtomicBoolean dataBasePopulated() { |
||||||
return dataBasePopulated; |
return dataBasePopulated; |
||||||
} |
} |
||||||
|
|
||||||
@Override |
@Override |
||||||
public AtomicBoolean dataBaseReady() { |
public AtomicBoolean dataBaseReady() { |
||||||
return dataBaseReady; |
return dataBaseReady; |
||||||
} |
} |
||||||
|
|
||||||
@Override |
@Override |
||||||
public final void populateDatabaseOnlyOnce() { |
public final void populateDatabaseOnlyOnce() { |
||||||
if (!dataBasePopulated().get()) { |
if (!dataBasePopulated().get()) { |
||||||
dataBasePopulated().set(true); |
dataBasePopulated().set(true); |
||||||
getMusicFolders().forEach(musicFolderDao::createMusicFolder); |
getMusicFolders().forEach(musicFolderDao::createMusicFolder); |
||||||
settingsService.clearMusicFolderCache(); |
settingsService.clearMusicFolderCache(); |
||||||
try { |
try { |
||||||
// Await time to avoid scan failure.
|
// Await time to avoid scan failure.
|
||||||
for (int i = 0; i < 10; i++) { |
for (int i = 0; i < 10; i++) { |
||||||
Thread.sleep(100); |
Thread.sleep(100); |
||||||
} |
} |
||||||
} catch (InterruptedException e) { |
} catch (InterruptedException e) { |
||||||
e.printStackTrace(); |
e.printStackTrace(); |
||||||
} |
} |
||||||
TestCaseUtils.execScan(mediaScannerService); |
TestCaseUtils.execScan(mediaScannerService); |
||||||
System.out.println("--- Report of records count per table ---"); |
System.out.println("--- Report of records count per table ---"); |
||||||
Map<String, Integer> records = TestCaseUtils.recordsInAllTables(daoHelper); |
Map<String, Integer> records = TestCaseUtils.recordsInAllTables(daoHelper); |
||||||
records.keySet().stream().filter(s -> |
records.keySet().stream().filter(s -> |
||||||
s.equals("MEDIA_FILE") |
s.equals("MEDIA_FILE") |
||||||
| s.equals("ARTIST") |
| s.equals("ARTIST") |
||||||
| s.equals("MUSIC_FOLDER") |
| s.equals("MUSIC_FOLDER") |
||||||
| s.equals("ALBUM")) |
| s.equals("ALBUM")) |
||||||
.forEach(tableName -> |
.forEach(tableName -> |
||||||
System.out.println("\t" + tableName + " : " + records.get(tableName).toString())); |
System.out.println("\t" + tableName + " : " + records.get(tableName).toString())); |
||||||
System.out.println("--- *********************** ---"); |
System.out.println("--- *********************** ---"); |
||||||
try { |
try { |
||||||
// Await for Lucene to finish writing(asynchronous).
|
// Await for Lucene to finish writing(asynchronous).
|
||||||
for (int i = 0; i < 5; i++) { |
for (int i = 0; i < 5; i++) { |
||||||
Thread.sleep(100); |
Thread.sleep(100); |
||||||
} |
} |
||||||
} catch (InterruptedException e) { |
} catch (InterruptedException e) { |
||||||
e.printStackTrace(); |
e.printStackTrace(); |
||||||
} |
} |
||||||
dataBaseReady().set(true); |
dataBaseReady().set(true); |
||||||
} else { |
} else { |
||||||
while (!dataBaseReady().get()) { |
while (!dataBaseReady().get()) { |
||||||
try { |
try { |
||||||
// The subsequent test method waits while reading DB data.
|
// The subsequent test method waits while reading DB data.
|
||||||
for (int i = 0; i < 10; i++) { |
for (int i = 0; i < 10; i++) { |
||||||
Thread.sleep(100); |
Thread.sleep(100); |
||||||
} |
} |
||||||
} catch (InterruptedException e) { |
} catch (InterruptedException e) { |
||||||
e.printStackTrace(); |
e.printStackTrace(); |
||||||
} |
} |
||||||
} |
} |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
} |
} |
@ -1,45 +1,45 @@ |
|||||||
package org.airsonic.player.service.search; |
package org.airsonic.player.service.search; |
||||||
|
|
||||||
import java.util.List; |
import java.util.List; |
||||||
import java.util.concurrent.atomic.AtomicBoolean; |
import java.util.concurrent.atomic.AtomicBoolean; |
||||||
|
|
||||||
import org.airsonic.player.domain.MusicFolder; |
import org.airsonic.player.domain.MusicFolder; |
||||||
import org.airsonic.player.util.MusicFolderTestData; |
import org.airsonic.player.util.MusicFolderTestData; |
||||||
|
|
||||||
/** |
/** |
||||||
* Test case interface for scanning MusicFolder. |
* Test case interface for scanning MusicFolder. |
||||||
*/ |
*/ |
||||||
public interface AirsonicHomeTest { |
public interface AirsonicHomeTest { |
||||||
|
|
||||||
/** |
/** |
||||||
* MusicFolder used by test class. |
* MusicFolder used by test class. |
||||||
* |
* |
||||||
* @return MusicFolder used by test class
|
* @return MusicFolder used by test class
|
||||||
*/ |
*/ |
||||||
default List<MusicFolder> getMusicFolders() { |
default List<MusicFolder> getMusicFolders() { |
||||||
return MusicFolderTestData.getTestMusicFolders(); |
return MusicFolderTestData.getTestMusicFolders(); |
||||||
}; |
}; |
||||||
|
|
||||||
/** |
/** |
||||||
* Whether the data input has been completed. |
* Whether the data input has been completed. |
||||||
* |
* |
||||||
* @return Static AtomicBoolean indicating whether the data injection has been |
* @return Static AtomicBoolean indicating whether the data injection has been |
||||||
* completed |
* completed |
||||||
*/ |
*/ |
||||||
abstract AtomicBoolean dataBasePopulated(); |
abstract AtomicBoolean dataBasePopulated(); |
||||||
|
|
||||||
/** |
/** |
||||||
* Whether the data input has been completed. |
* Whether the data input has been completed. |
||||||
* |
* |
||||||
* @return Static AtomicBoolean indicating whether the data injection has been |
* @return Static AtomicBoolean indicating whether the data injection has been |
||||||
* completed |
* completed |
||||||
*/ |
*/ |
||||||
abstract AtomicBoolean dataBaseReady(); |
abstract AtomicBoolean dataBaseReady(); |
||||||
|
|
||||||
/** |
/** |
||||||
* Populate the database only once. |
* Populate the database only once. |
||||||
* It is called in the @Before granted method. |
* It is called in the @Before granted method. |
||||||
*/ |
*/ |
||||||
void populateDatabaseOnlyOnce(); |
void populateDatabaseOnlyOnce(); |
||||||
|
|
||||||
} |
} |
||||||
|
Loading…
Reference in new issue