]> WPIA git - gigi.git/blob - src/club/wpia/gigi/dbObjects/Name.java
fbcf10eed4c94fc76c8c975102e339e31c83f7a8
[gigi.git] / src / club / wpia / gigi / dbObjects / Name.java
1 package club.wpia.gigi.dbObjects;
2
3 import java.io.PrintWriter;
4 import java.util.Map;
5
6 import club.wpia.gigi.GigiApiException;
7 import club.wpia.gigi.database.DBEnum;
8 import club.wpia.gigi.database.GigiPreparedStatement;
9 import club.wpia.gigi.database.GigiResultSet;
10 import club.wpia.gigi.dbObjects.NamePart.NamePartType;
11 import club.wpia.gigi.localisation.Language;
12 import club.wpia.gigi.output.template.Outputable;
13 import club.wpia.gigi.util.HTMLEncoder;
14
15 public class Name implements Outputable, IdCachable {
16
17     public static enum NameSchemaType implements DBEnum {
18         SINGLE, WESTERN;
19
20         @Override
21         public String getDBName() {
22             return toString().toLowerCase();
23         }
24
25     }
26
27     private abstract static class SchemedName {
28
29         /**
30          * @see Name#matches(String)
31          */
32         public abstract boolean matches(String text);
33
34         public abstract String toPreferredString();
35
36         /**
37          * @see Name#toAbbreviatedString()
38          */
39         public abstract String toAbbreviatedString();
40
41         public abstract NameSchemaType getSchemeName();
42
43         /**
44          * @see Name#output(PrintWriter, Language, Map)
45          */
46         public abstract void output(PrintWriter out);
47
48         /**
49          * @see Name#toInitialsString()
50          */
51         public abstract String toInitialsString();
52
53     }
54
55     private static class SingleName extends SchemedName {
56
57         private NamePart singlePart;
58
59         public SingleName(NamePart singlePart) {
60             this.singlePart = singlePart;
61         }
62
63         @Override
64         public boolean matches(String text) {
65             return text.equals(singlePart.getValue());
66         }
67
68         @Override
69         public String toPreferredString() {
70             return singlePart.getValue();
71         }
72
73         @Override
74         public String toAbbreviatedString() {
75             return singlePart.getValue();
76         }
77
78         @Override
79         public String toInitialsString() {
80             return singlePart.getValue().substring(0, 1);
81         }
82
83         @Override
84         public NameSchemaType getSchemeName() {
85             return NameSchemaType.SINGLE;
86         }
87
88         @Override
89         public void output(PrintWriter out) {
90             out.print("<span class='sname'>");
91             out.print(HTMLEncoder.encodeHTML(singlePart.getValue()));
92             out.print("</span>");
93         }
94     }
95
96     /**
97      * Naming scheme where any first name and the first last name is required.
98      * Requires first names in arbitrary order. Last names and suffixes in
99      * correct order.
100      */
101     private static class WesternName extends SchemedName {
102
103         private NamePart[] firstNames;
104
105         private NamePart[] lastNames;
106
107         private NamePart[] suffixes;
108
109         public WesternName(NamePart[] firstName, NamePart lastName[], NamePart[] suffixes) {
110             if (lastName.length < 1 || firstName.length < 1) {
111                 throw new Error("Requires at least one first and one last name");
112             }
113             this.lastNames = lastName;
114             this.firstNames = firstName;
115             this.suffixes = suffixes;
116         }
117
118         @Override
119         public boolean matches(String text) {
120             String[] tokens = text.split(" ");
121
122             NamePart mandatoryLN = lastNames[0];
123             for (int i = 0; i < tokens.length; i++) {
124                 if (tokens[i].equals(mandatoryLN.getValue())) {
125                     if (tryMatchFirst(tokens, i) && tryMatchLastSuff(tokens, i)) {
126                         return true;
127                     }
128
129                 }
130             }
131             return false;
132         }
133
134         private boolean tryMatchLastSuff(String[] tokens, int lastName) {
135             int userInputPos = lastName + 1;
136             boolean currentlyMatchingLastNames = true;
137             int referencePos = 1;
138             while ((currentlyMatchingLastNames || referencePos < suffixes.length) && (userInputPos < tokens.length)) {
139                 // we break when we match suffixes and there is no
140                 // reference-suffix left
141                 if (currentlyMatchingLastNames) {
142                     if (referencePos >= lastNames.length) {
143                         referencePos = 0;
144                         currentlyMatchingLastNames = false;
145                     } else if (tokens[userInputPos].equals(lastNames[referencePos].getValue())) {
146                         userInputPos++;
147                         referencePos++;
148                     } else {
149                         referencePos++;
150                     }
151                 } else {
152                     if (tokens[userInputPos].equals(suffixes[referencePos].getValue())) {
153                         userInputPos++;
154                         referencePos++;
155                     } else {
156                         referencePos++;
157                     }
158                 }
159             }
160             if (userInputPos >= tokens.length) {
161                 // all name parts are covered we're done here
162                 return true;
163             }
164             return false;
165         }
166
167         private boolean tryMatchFirst(String[] tokens, int lastName) {
168             if (lastName == 0) {
169                 return false;
170             }
171             boolean[] fnUsed = new boolean[firstNames.length];
172             for (int i = 0; i < lastName; i++) {
173                 boolean found = false;
174                 for (int j = 0; j < fnUsed.length; j++) {
175                     if ( !fnUsed[j] && firstNames[j].getValue().equals(tokens[i])) {
176                         fnUsed[j] = true;
177                         found = true;
178                         break;
179                     }
180                 }
181                 if ( !found) {
182                     return false;
183                 }
184             }
185             return true;
186         }
187
188         @Override
189         public String toPreferredString() {
190             StringBuilder res = new StringBuilder();
191             appendArray(res, firstNames);
192             appendArray(res, lastNames);
193             appendArray(res, suffixes);
194             res.deleteCharAt(res.length() - 1);
195             return res.toString();
196         }
197
198         @Override
199         public void output(PrintWriter out) {
200             outputNameParts(out, "fname", firstNames, false);
201             outputNameParts(out, "lname", lastNames, true);
202             outputNameParts(out, "suffix", suffixes, true);
203         }
204
205         private void outputNameParts(PrintWriter out, String type, NamePart[] input, boolean leadingSpace) {
206             StringBuilder res;
207             res = new StringBuilder();
208             appendArray(res, input);
209             if (res.length() > 0) {
210                 res.deleteCharAt(res.length() - 1);
211                 if (leadingSpace) {
212                     out.print(" ");
213                 }
214                 out.print("<span class='" + type + "'>");
215                 out.print(HTMLEncoder.encodeHTML(res.toString()));
216                 out.print("</span>");
217             }
218         }
219
220         private void appendArray(StringBuilder res, NamePart[] ps) {
221             for (int i = 0; i < ps.length; i++) {
222                 res.append(ps[i].getValue());
223                 res.append(" ");
224             }
225         }
226
227         @Override
228         public NameSchemaType getSchemeName() {
229             return NameSchemaType.WESTERN;
230         }
231
232         @Override
233         public String toAbbreviatedString() {
234             return firstNames[0].getValue() + " " + lastNames[0].getValue().charAt(0) + ".";
235         }
236
237         @Override
238         public String toInitialsString() {
239
240             String initals = getInitialByNamePart(firstNames, lastNames, suffixes);
241
242             return initals;
243         }
244     }
245
246     private int id;
247
248     private int ownerId;
249
250     /**
251      * Only resolved lazily to resolve circular referencing with {@link User} on
252      * {@link User#getPreferredName()}. Resolved based on {@link #ownerId}.
253      */
254     private User owner;
255
256     private NamePart[] parts;
257
258     private SchemedName scheme;
259
260     /**
261      * This name should not get verifed anymore and therefore not be displayed
262      * to the RA-Agent. This state is irrevocable.
263      */
264     private boolean deprecated;
265
266     private Name(GigiResultSet rs) {
267         ownerId = rs.getInt(1);
268         id = rs.getInt(2);
269         deprecated = rs.getString("deprecated") != null;
270         try (GigiPreparedStatement partFetcher = new GigiPreparedStatement("SELECT `type`, `value` FROM `nameParts` WHERE `id`=? ORDER BY `position` ASC", true)) {
271             partFetcher.setInt(1, id);
272             GigiResultSet rs1 = partFetcher.executeQuery();
273             rs1.last();
274             NamePart[] dt = new NamePart[rs1.getRow()];
275             rs1.beforeFirst();
276             for (int i = 0; rs1.next(); i++) {
277                 dt[i] = new NamePart(rs1);
278             }
279             parts = dt;
280             scheme = detectScheme();
281         }
282
283     }
284
285     public Name(User u, NamePart... np) throws GigiApiException {
286         synchronized (Name.class) {
287             parts = np;
288             owner = u;
289             scheme = detectScheme();
290             if (scheme == null) {
291                 throw new GigiApiException("Name particles don't match up for any known name scheme.");
292             }
293             try (GigiPreparedStatement inserter = new GigiPreparedStatement("INSERT INTO `names` SET `uid`=?, `type`=?::`nameSchemaType`")) {
294                 inserter.setInt(1, u.getId());
295                 inserter.setEnum(2, scheme.getSchemeName());
296                 inserter.execute();
297                 id = inserter.lastInsertId();
298             }
299             try (GigiPreparedStatement inserter = new GigiPreparedStatement("INSERT INTO `nameParts` SET `id`=?, `position`=?, `type`=?::`namePartType`, `value`=?")) {
300                 inserter.setInt(1, id);
301                 for (int i = 0; i < np.length; i++) {
302                     inserter.setInt(2, i);
303                     inserter.setEnum(3, np[i].getType());
304                     inserter.setString(4, np[i].getValue());
305                     inserter.execute();
306                 }
307             }
308             cache.put(this);
309         }
310     }
311
312     private SchemedName detectScheme() {
313         if (parts.length == 1 && parts[0].getType() == NamePartType.SINGLE_NAME) {
314             return new SingleName(parts[0]);
315         }
316         int suffixCount = 0;
317         int lastCount = 0;
318         int firstCount = 0;
319         int stage = 0;
320         for (NamePart p : parts) {
321             if (p.getType() == NamePartType.LAST_NAME) {
322                 lastCount++;
323                 if (stage < 1) {
324                     stage = 1;
325                 } else if (stage != 1) {
326                     return null;
327                 }
328             } else if (p.getType() == NamePartType.FIRST_NAME) {
329                 firstCount++;
330                 if (stage != 0) {
331                     return null;
332                 }
333             } else if (p.getType() == NamePartType.SUFFIX) {
334                 suffixCount++;
335                 if (stage < 2) {
336                     stage = 2;
337                 } else if (stage != 2) {
338                     return null;
339                 }
340
341             } else {
342                 return null;
343             }
344         }
345         if (firstCount == 0 || lastCount == 0) {
346             return null;
347         }
348         NamePart[] firstNames = new NamePart[firstCount];
349         NamePart[] lastNames = new NamePart[lastCount];
350         NamePart[] suffixes = new NamePart[suffixCount];
351         int fn = 0;
352         int ln = 0;
353         int sn = 0;
354         for (NamePart p : parts) {
355             if (p.getType() == NamePartType.FIRST_NAME) {
356                 firstNames[fn++] = p;
357             } else if (p.getType() == NamePartType.SUFFIX) {
358                 suffixes[sn++] = p;
359             } else if (p.getType() == NamePartType.LAST_NAME) {
360                 lastNames[ln++] = p;
361             }
362         }
363
364         return new WesternName(firstNames, lastNames, suffixes);
365     }
366
367     /**
368      * Outputs an HTML variant suitable for locations where special UI features
369      * should indicate the different Name Parts.
370      */
371     @Override
372     public void output(PrintWriter out, Language l, Map<String, Object> vars) {
373         out.print("<span class=\"names\">");
374         scheme.output(out);
375         out.print("</span>");
376     }
377
378     /**
379      * Tests, if this name fits into the given string.
380      * 
381      * @param text
382      *            the name to test against
383      * @return true, iff this name matches.
384      */
385     public boolean matches(String text) {
386         if ( !text.equals(text.trim())) {
387             return false;
388         }
389         return scheme.matches(text);
390     }
391
392     @Override
393     public String toString() {
394         return scheme.toPreferredString();
395     }
396
397     /**
398      * Transforms this String into a short form. This short form should not be
399      * unique. (For "western" names this would be "firstName
400      * firstCharOfLastName.".)
401      * 
402      * @return the short form of the name
403      */
404     public String toAbbreviatedString() {
405         return scheme.toAbbreviatedString();
406     }
407
408     /**
409      * Transforms this Name object into a short form. This short form might not
410      * be unique. (For "western" names this would be all first letters of each
411      * name part)
412      * 
413      * @return the short form of the name
414      */
415     public String toInitialsString() {
416         return scheme.toInitialsString();
417     }
418
419     public int getVerificationPoints() {
420         try (GigiPreparedStatement query = new GigiPreparedStatement("SELECT SUM(`points`) FROM (SELECT DISTINCT ON (`from`, `method`) `points` FROM `notary` WHERE `to`=? AND `deleted` IS NULL AND (`expire` IS NULL OR `expire` > CURRENT_TIMESTAMP) ORDER BY `from`, `method`, `when` DESC) AS p")) {
421             query.setInt(1, getId());
422
423             GigiResultSet rs = query.executeQuery();
424             int points = 0;
425
426             if (rs.next()) {
427                 points = rs.getInt(1);
428             }
429
430             return points;
431         }
432     }
433
434     @Override
435     public int getId() {
436         return id;
437     }
438
439     private static ObjectCache<Name> cache = new ObjectCache<>();
440
441     public synchronized static Name getById(int id) {
442         Name cacheRes = cache.get(id);
443         if (cacheRes != null) {
444             return cacheRes;
445         }
446
447         try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `uid`, `id`, `deprecated` FROM `names` WHERE `deleted` IS NULL AND `id` = ?")) {
448             ps.setInt(1, id);
449             GigiResultSet rs = ps.executeQuery();
450             if ( !rs.next()) {
451                 return null;
452             }
453
454             Name c = new Name(rs);
455             cache.put(c);
456             return c;
457         }
458     }
459
460     public NamePart[] getParts() {
461         return parts;
462     }
463
464     public void remove() {
465         synchronized (Name.class) {
466             cache.remove(this);
467             try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `names` SET `deleted` = now() WHERE `id`=?")) {
468                 ps.setInt(1, id);
469                 ps.executeUpdate();
470             }
471         }
472     }
473
474     public synchronized void deprecate() {
475         deprecated = true;
476         try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `names` SET `deprecated`=now() WHERE `id`=?")) {
477             ps.setInt(1, id);
478             ps.executeUpdate();
479         }
480     }
481
482     public boolean isDeprecated() {
483         return deprecated;
484     }
485
486     public synchronized User getOwner() {
487         if (owner == null) {
488             owner = User.getById(ownerId);
489         }
490         return owner;
491     }
492
493     private static String getInitialByNamePart(NamePart[]... npa) {
494         StringBuilder initals = new StringBuilder();
495         for (NamePart[] np : npa) {
496             initals.append(getInitialByNamePart(np));
497         }
498         return initals.toString();
499     }
500
501     private static String getInitialByNamePart(NamePart[] np) {
502         StringBuilder initals = new StringBuilder();
503         for (NamePart p : np) {
504             switch (p.getValue()) {
505             case "-":
506             case "/":
507                 break;
508             default:
509                 initals.append(p.getValue().substring(0, 1).toUpperCase());
510                 break;
511             }
512         }
513         return initals.toString();
514     }
515 }