package dnb.data.impl;
import java.util.List;
import java.util.logging.Logger;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Order;
import dnb.analyze.SimilarScanResult;
import dnb.data.Artist;
import dnb.data.Genre;
import dnb.data.Label;
import dnb.data.Repository;
import dnb.data.RepositoryObject;
import dnb.util.InsertionSort;
import dnb.util.MatchUtils;
import dnb.util.MatchUtils.MatchUtilsResult;
/** XXX fat client issues: https://www.hibernate.org/333.html
* WEAPON of choice: Implicit Humongous Transation
* We have a single session, we assume that all object transactions, even lazy reads,
* are made thread safe somehow.
* Then we have this single session open and immediately begin a transaction.
* When we are at the point where a transaction needs to be committed,
* we commit all changes that have happened to the object graph in memory,
* then immediately begin a new transaction and carry on in the same session.
This is basically the same thing that you have when working on an SQL command line:
you do selects, inserts and updates, and then you write "COMMIT;" to commit all that you did.
After committing a new transaction is immediately open.
We will just be careful in our application not to change objects around inadvertently outside
of what we call a command (as in Command Pattern).
RIGHT: => Natural changes only using GET/SET ALWAYS require PropertyChangeListener overhead!!!
==> Modify objects, pass them to PersistenceFacade#save for saving & propagating change events.
*
*
*/
public class RepositoryHibernateImpl implements Repository {
/**
* Levenshtein distance for artist similarity search.
*/
private static final int LDISTANCE_ARTIST = 1;
/**
* Levenshtein distance for label similarity search.
*/
private static final int LDISTANCE_LABEL = 1;
private static final Logger LOGGER = Logger.getLogger(MatchUtils.class.getName());
/**
* Levenshtein distance for genre similarity search.
*/
private static final int LDISTANCE_GENRE = 3;
// linked lists for better insertion sort
private List<Label> labels;
private List<Artist> artists;
private List<Genre> genres;
private final SessionFactory sessionFactory;
public RepositoryHibernateImpl(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
load();
}
@SuppressWarnings("unchecked")
private void load() {
Session s = sessionFactory.openSession();
s.beginTransaction();
labels = s.createCriteria(LabelHibernateImpl.class).addOrder(Order.desc("name")).list();
artists = s.createCriteria(ArtistHibernateImpl.class).addOrder(Order.desc("name")).list();
genres = s.createCriteria(GenreHibernateImpl.class).addOrder(Order.desc("name")).list();
s.getTransaction().commit();
s.close();
// hook up labels 4 saving and labelcode callback
for (Label l : labels) {
((LabelHibernateImpl)l).postLoad(this);
}
}
/* (non-Javadoc)
* @see dnb.data.Repository#getLabel(java.lang.String)
*/
@SuppressWarnings("unchecked")
public Label getLabel(String name) {
if (name == null || name.isEmpty()) {
return null;
}
for (Label l : labels) {
if (l.getName().equalsIgnoreCase(name)) {
return l;
}
}
Label l = new LabelHibernateImpl(this, name);
List<?> ll = labels; // old trick, we know Genre is a repo object
InsertionSort.insert((List<RepositoryObjectHibernateImpl>)ll, (RepositoryObjectHibernateImpl) l);
save(l);
return l;
}
/* (non-Javadoc)
* @see dnb.data.Repository#getArtist(java.lang.String)
*/
@SuppressWarnings("unchecked")
public Artist getArtist(String artist) {
if (artist == null || artist.isEmpty()) {
return null;
}
for (Artist a : artists) {
if (a.getName().equalsIgnoreCase(artist)) {
return a; // already contained
}
}
Artist a = new ArtistHibernateImpl(artist);
List<?> l = artists; // old trick, we know Genre is a repo object
InsertionSort.insert((List<RepositoryObjectHibernateImpl>)l, (RepositoryObjectHibernateImpl) a);
save(a);
return a;
}
/* (non-Javadoc)
* @see dnb.data.Repository#getGenre(java.lang.String)
*/
@SuppressWarnings("unchecked")
public Genre getGenre(String genre) {
if (genre == null || genre.isEmpty()) {
return null;
}
for (Genre g : genres) {
if (g.getName().equalsIgnoreCase(genre)) {
return g; // already contained
}
}
Genre g = new GenreHibernateImpl(genre);
List l = genres; // old trick, we know Genre is a repo object
InsertionSort.insert((List<RepositoryObjectHibernateImpl>)l, (RepositoryObjectHibernateImpl) g);
save(g);
return g;
}
void save(Object a) {
Session s = sessionFactory.openSession();
s.beginTransaction();
s.saveOrUpdate(a);
s.getTransaction().commit();
s.close();
}
Label isLabelCodeAssigned(Label l, String labelcode) {
List<LabelcodeHibernateImpl> codes;
for (Label la : labels) {
if (la == l) { // test for identity is OK => all repo objects r unique
continue; // don't check self
}
codes = ((LabelHibernateImpl)la).getCodes();
for (LabelcodeHibernateImpl lc : codes) {
if (lc.getName().equalsIgnoreCase(labelcode)) {
return la; // already contained
}
}
}
return null;
}
public Label findByLabelcode(String labelcode) {
List<LabelcodeHibernateImpl> codes;
for (Label la : labels) {
codes = ((LabelHibernateImpl)la).getCodes();
for (LabelcodeHibernateImpl lc : codes) {
if (lc.getName().equalsIgnoreCase(labelcode)) {
return la; // already contained
}
}
}
return null;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
private static int compare(final char c, String key) {
return (int)c - Character.toLowerCase(key.charAt(0));
}
/**
* XXX possible bug source, test !
* @param l
* @param c must be a lowercase letter !
* @return
*/
@SuppressWarnings("unchecked")
private static int findStartOf(final List<RepositoryObjectHibernateImpl> l, final char c) {
if(l.isEmpty()) {
return -1;
}
InsertionSort.Finder<String> f = new InsertionSort.Finder<String>() {
@Override public int compareTo(String key) {
return compare(c, key);
}};
InsertionSort.Converter<RepositoryObject, String> conv
= new InsertionSort.Converter<RepositoryObject, String>() {
@Override public String convert(RepositoryObject source) {
return source.getName();
}};
int idx = InsertionSort.find((List)l, conv, f, 0, l.size());
if(f.compareTo(conv.convert(l.get(idx))) == 0) {
//ok, found, but maybe in the middle of the list, walk backwards until start is found
while(idx >= 0 && f.compareTo(conv.convert(l.get(idx))) == 0) {
idx--;
}
return ++idx;
} else {
// maybe insert location is b4, check
idx++;
if (idx < l.size()) {
if(f.compareTo(conv.convert(l.get(idx))) == 0) {
return idx;
} else {
return -1;
}
} else {
return -1;
}
}
}
@SuppressWarnings("unchecked")
private static <T extends RepositoryObject> SimilarScanResult<T> findSimilar(List<T> list, String name) {
int dst;
String ln = name.toLowerCase(); // cache for distance
SimilarScanResult<T> ssr = null;
SimilarScanResult.Match<T> mt;
// brute force 4 labels 2 get max match probability ;-)
for (T a : list) {
// distance only
dst = StringUtils.getLevenshteinDistance(ln, a.getName().toLowerCase());
if (dst == 0) {
mt = new SimilarScanResult.Match<T>(SimilarScanResult.MatchType.EXACT, a.getName(), a);
if (ssr == null) { ssr = new SimilarScanResult<T>(mt); } else { ssr.add(mt); }
return ssr; // => we got an exact match => cancel search
} else if (dst <= LDISTANCE_ARTIST) {
mt = new SimilarScanResult.Match<T>(dst, a.getName(), a);
if (ssr == null) { ssr = new SimilarScanResult<T>(mt); } else { ssr.add(mt); }
} // else next label...
}
return ssr;
}
// finds an artist similar to the passed one
/* (non-Javadoc)
* @see dnb.data.Repository#findSimilar(java.lang.String, int)
*/
@SuppressWarnings("unchecked")
public SimilarScanResult<Artist> findSimilarArtist(String name) {
return findSimilar(artists, name);
}
@SuppressWarnings("unchecked")
public SimilarScanResult<Genre> findSimilarGenre(String name) {
return findSimilar(genres, name);
}
@SuppressWarnings("unchecked")
public SimilarScanResult<Label> findSimilarLabel(String name) {
SimilarScanResult<Label> ssr = null;
SimilarScanResult.Match<Label> mt;
String pt = null;
int dst;
// normalize & split passed name
MatchUtilsResult in = MatchUtils.getPartsNormalize(name);
// brute force 4 labels 2 get max match propability ;-)
LabelHibernateImpl l;
for (Label ll : labels) {
l = (LabelHibernateImpl) ll;
// get distance of the normed names 2 each other
dst = StringUtils.getLevenshteinDistance(in.getNormalized(), l.getNormalizedName());
if (dst == 0) {
LOGGER.info("Found exact match for label " + name);
mt = new SimilarScanResult.Match<Label>(SimilarScanResult.MatchType.EXACT, ll.getName(), ll);
if (ssr == null) { ssr = new SimilarScanResult<Label>(mt); } else { ssr.add(mt); }
return ssr; // => we got an exact match => cancel search
} else if (dst <= LDISTANCE_LABEL) {
LOGGER.info("Found similar match:" + name);
mt = new SimilarScanResult.Match<Label>(dst, ll.getName(), ll);
if (ssr == null) { ssr = new SimilarScanResult<Label>(mt); } else { ssr.add(mt); }
} else { // no relevant distance => check part match
if (in.getParts() != null) { // we got multiple parts in the passed string => try part find
pt = l.matchParts(in.getParts()); // match the current label against the words of this label name
if (pt != null) { // part match found
LOGGER.info("Found part match '" + pt + "' for label " + l.getName());
mt = new SimilarScanResult.Match<Label>(SimilarScanResult.MatchType.PART, pt, ll);
if (ssr == null) { ssr = new SimilarScanResult<Label>(mt); } else { ssr.add(mt); }
}
}
}
// else next label...
}
LOGGER.info("No match for '" + name + "'");
return ssr;
}
public List<Label> getLabels() {
return labels;
}
public List<Artist> getArtists() {
return artists;
}
public List<Genre> getGenres() {
return genres;
}
}