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