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