View Javadoc

1   package org.musicontroller.core;
2   
3   import java.util.Collection;
4   import java.util.LinkedList;
5   import java.util.List;
6   import java.util.Set;
7   import java.util.TreeSet;
8   
9   /**
10   * A Keyword is a namable property of a Song. Keywords are hierarchical objects.
11   * Their ordering is lexicographical after their name. The keyword names are case insensitive.
12   * @author Varienaja
13   */
14  public class Keyword extends LinkableAbs implements Comparable<Keyword> {
15  	private Keyword _parent;
16  	private Set<Keyword> _children;
17  	
18  	// The maximal similarity between keywords. 
19  	public static final int _maxSimilarityPower = 7;
20  	public static final int _maxSimilarity = 1<<_maxSimilarityPower;
21  	
22  	public Keyword() {
23  		_parent = null;
24  		_children = null;
25  	}
26  	
27  	/**
28  	 * Factory method constructs a keyword with the given
29  	 * name and a null parent.
30  	 * @param name
31  	 * @return The Keyword
32  	 */
33  	public static Keyword construct(String name) {
34  		Keyword me = new Keyword();
35  		me.setName(name);
36  		return me;
37  	}
38  
39  	/**
40  	 * Factory method constructs a keyword with the given
41  	 * name and parent.
42  	 * @param name
43  	 * @return The Keyword
44  	 */
45  	public static Keyword construct(String name, Keyword parent) {
46  		Keyword me = new Keyword();
47  		me.setName(name);
48  		try {
49  			me.setParent(parent);
50  		} catch (KeywordCycleException e) {
51  			// This is impossible: me is a freshly generated keyword and cannot be a parent of the parent.
52  		}
53  		return me;
54  	}
55  
56  	public Keyword getParent() {
57  		return _parent;
58  	}
59  	
60  	public void setParent(Keyword parent) throws KeywordCycleException {
61  		if(parent!=null && parent.isChildOf(this)) {
62  			throw new KeywordCycleException();
63  		}
64  		this._parent = parent;
65  	}
66  	
67  	/**
68  	 * Determines if this keyword is a child of another
69  	 * keyword. Used to prevent cyclic parent relationships.
70  	 * 
71  	 * @param other The other keyword.
72  	 * @return True if this keyword is a child of the other keyword, or false otherwise.
73  	 */
74  	public boolean isChildOf(Keyword other) {
75  		if(other==null) {
76  			return false;
77  		}
78  		if(this.equals(other)) {
79  			return false;
80  		}
81  		if(this.getParent()==null) {
82  			return false;
83  		}
84  		if(this.getParent().equals(other)) {
85  			return true;
86  		}
87  		return this.getParent().isChildOf(other);
88  	}
89  	
90  	/**
91  	 * Define Keyword business equality. Two keywords are equal
92  	 * is there names are equal (disregarding case) and their parents are equal.
93  	 */
94  	@Override
95  	public boolean equals(Object obj) {
96  		if (this==obj) {
97  			return true;
98  		}
99  		if (obj instanceof Keyword) {
100 			Keyword other = (Keyword) obj;
101 			boolean result = this.getName().equalsIgnoreCase(other.getName()) && 
102 			    (
103 			    		(this.getParent()==null && other.getParent()==null)
104 			    	||	(this.getParent() != null && (this.getParent().equals(other.getParent())))
105 			    );
106 			return result;
107 		} else {
108 			return false;
109 		}
110 	}
111 	
112 	@Override
113 	public int hashCode() {
114 		int result = 0;
115 		if(getName()!=null) {
116 			result += getName().toLowerCase().hashCode();
117 		}
118 		if(getParent()!=null) {
119 			result = (17 * result) + getParent().hashCode();
120 		}
121 		return result;
122 	}
123 	
124 	/**
125 	 * A short human readable representation of the keyword.
126 	 */
127 	public String toString() {
128 		return this.getName();
129 	}
130 	
131 	/**
132 	 * Calculates similarity between keywords. The more parents the two keywords have in common,
133 	 * the more similar they are. The similarity is defined as the length of the common chain
134 	 * of parents the keyword share.
135 	 * <ul>
136 	 * <li>Equal keywords are maximally similar.
137 	 * <li>Keywords without a common parent are not similar at all.
138 	 * <li>Generalisation: Every step from keyword to its parent needed to arrive at a common
139 	 *  ancestor reduces the similarity.
140 	 * <li>Specialisation: The keyword is a parent of the keyword it is compared with.
141 	 * <li>The length of the chain of common ancestors is not relevant.
142 	 * </ul>
143 	 * @param keyword The Keyword to compare this one to. This may not be NULL (will throw NullPointerException).
144 	 * @return The amount of similar parents.
145 	 */
146 	public int similarityScore(Keyword keyword,StringBuilder sbout) {
147 		if(this.equals(keyword)) {
148 			return 1<<_maxSimilarityPower ;
149 		} else {
150 			boolean commonAncestorFound = false;
151 			List<Keyword> myLineage = calcLineage();
152 			List<Keyword> othersLineage = keyword.calcLineage();
153 			StringBuilder sb = new StringBuilder();			
154 			sb.append("<");
155 			sb.append("kw1:");
156 			sb.append(myLineage.toString());
157 			sb.append(",kw2:");
158 			sb.append(othersLineage.toString());
159 			int generalisations = 0;
160 			int specialisations = 0;
161 			for (Keyword p1 : myLineage) {
162 				// Check if the other keyword is a specialisation of myself. 
163 				specialisations = 0;
164 				for (Keyword p2 : othersLineage) {
165 					if (p1.equals(p2)) {
166 						commonAncestorFound = true;
167 					}
168 					if(commonAncestorFound) {
169 						break;
170 					}
171 					specialisations++;
172 				}
173 				if(commonAncestorFound) {
174 					break;
175 				}
176 				// The other keyword can only be a specialization of a generalisation of myself.
177 				// Reduce the similarity.
178 				generalisations++;
179 			}
180 			if(commonAncestorFound) {
181 				sb.append(".G:");
182 				sb.append(generalisations);
183 				sb.append(",S:");
184 				sb.append(specialisations);
185 				sb.append("->");
186 				sb.append((1 << (_maxSimilarityPower - generalisations)) - specialisations);
187 				sb.append(">");
188 				sbout.append(sb.toString());
189 			}
190 			int similarity = calculateSimilarity(commonAncestorFound,generalisations,specialisations);
191 			return similarity;
192 		}
193 	}
194 
195 	/**
196 	 * calculate the similarity score based on the number of generalisations needed to
197 	 * get from one keyword to another. 
198 	 * 
199 	 * @param commonAncestorFound True if the compared keyword have a shared ancestor.
200 	 * @param generalisations The number of generalisations.
201 	 * @param specialisations The number of specialisations.
202 	 * @return The calculated keyword similarity.
203 	 */
204 	public static int calculateSimilarity(boolean commonAncestorFound, int generalisations, int specialisations) {
205 		if(!commonAncestorFound) {
206 			return 0;
207 		}
208 		int remainingSimilarityPower = _maxSimilarityPower - generalisations;
209 		int specialisationPenalty = 0;
210 		if(remainingSimilarityPower<1) {
211 			specialisationPenalty = 1;
212 		} else {
213 			specialisationPenalty = (1<< (remainingSimilarityPower-1));
214 		}
215 		if(remainingSimilarityPower<0) {
216 			remainingSimilarityPower = 0;
217 		}
218 		return (1 << remainingSimilarityPower) - (specialisationPenalty * specialisations);
219 	}
220 
221 	/**
222 	 * Lists the entire path of parents of this keyword. The path ends iff:
223 	 * <ul>
224 	 * <li> The parent is NULL.
225 	 * <li> The parent is equal to the keyword.
226 	 * </ul>
227 	 * @return A List containing this, parent-Keyword, grandparent-Keyword, etc.
228 	 */
229 	private List<Keyword> calcLineage() {
230 		List<Keyword> l = new LinkedList<Keyword>();
231 		l.add(this);
232 		
233 		Keyword p = getParent();
234 		while ((p!=null) && (this!=p)) {
235 			l.add(p);
236 			p = p.getParent();
237 		}
238 		
239 		return l;
240 	}
241 
242 	public String getType() {
243 		return "K";
244 	}
245 	
246 	/**
247 	 * The (non-persistent) Children of this Keyword.
248 	 * @return This Keywords' child-keywords.
249 	 */
250 	public Set<Keyword> getChildren() {
251 		return _children;
252 	}
253 	
254 	/**
255 	 * Adds a Keyword as a Child of this Keyword. The Childs parent must already
256 	 * be set to this Keyword. The Children-property of a Keyword is 
257 	 * non-persistent, and therefore uninitialized after reading a Keyword from
258 	 * persistent storage.
259 	 * @param keyword The (non-null) Child.
260 	 * @return Whether addition to the Children was successful.
261 	 */
262 	public boolean addChild(Keyword keyword) {
263 		if (keyword==null) return false; //No rubbish.
264 		if (!keyword._parent.equals(this)) return false;
265 		
266 		if (_children == null) {
267 			_children = new TreeSet<Keyword>();
268 		}
269 		return _children.add(keyword);
270 	}
271 	
272 	/**
273 	 * Constructs a Set of Keywords which are all topmost Keywords in the 
274 	 * hierarchical Keyword-ordering. The Keywords in the resulting Set all
275 	 * have their non-persistent children-properties initialized. The returned
276 	 * set is in lexicographical order.
277 	 * @param keywords All Keywords.
278 	 * @return The Set of topmost Keywords.
279 	 */
280 	public static Set<Keyword> constructHierarchy(Collection<Keyword> keywords) {
281 		Set<Keyword> result = new TreeSet<Keyword>();
282 		
283 		//Iterate through all Keywords. Keywords without a Parent are topmost.
284 		//Every other Keyword is someones Child. We will first insert its
285 		//topmost parent into the resultset, and then insert the Child at the
286 		//correct position.
287 		for (Keyword keyword : keywords) {
288 			if (keyword.getParent()==null) {
289 				result.add(keyword);
290 			} else {
291 				keyword.getParent().addChild(keyword);
292 			}
293 		}
294 		
295 		return result;
296 	}
297 	
298 
299 	/**
300 	 * Returns the amount of children (including grandchildren, etc) of this
301 	 * parent. The Hierarchy of the Keywords must have been constructed,
302 	 * otherwise the result of this method will be 0.
303 	 * @return The amount of Children.
304 	 */
305 	public int getChildCount() {
306 		if (_children==null) return 0;
307 		
308 		int sum = 0;
309 		for (Keyword child : _children) {
310 			sum += 1 + child.getChildCount();
311 		}
312 		return sum;
313 	}
314 
315 	/*
316 	 * (non-Javadoc)
317 	 * @see java.lang.Comparable#compareTo(java.lang.Object)
318 	 */
319 	public int compareTo(Keyword other) {
320 		return getName().toLowerCase().compareTo(other.getName().toLowerCase());
321 	}
322 
323 }