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