1 package club.wpia.gigi.dbObjects;
3 import java.io.PrintWriter;
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;
15 public class Name implements Outputable, IdCachable {
17 public static enum NameSchemaType implements DBEnum {
21 public String getDBName() {
22 return toString().toLowerCase();
27 private abstract static class SchemedName {
30 * @see Name#matches(String)
32 public abstract boolean matches(String text);
34 public abstract String toPreferredString();
37 * @see Name#toAbbreviatedString()
39 public abstract String toAbbreviatedString();
41 public abstract NameSchemaType getSchemeName();
44 * @see Name#output(PrintWriter, Language, Map)
46 public abstract void output(PrintWriter out);
49 * @see Name#toInitialsString()
51 public abstract String toInitialsString();
55 private static class SingleName extends SchemedName {
57 private NamePart singlePart;
59 public SingleName(NamePart singlePart) {
60 this.singlePart = singlePart;
64 public boolean matches(String text) {
65 return text.equals(singlePart.getValue());
69 public String toPreferredString() {
70 return singlePart.getValue();
74 public String toAbbreviatedString() {
75 return singlePart.getValue();
79 public String toInitialsString() {
80 return singlePart.getValue().substring(0, 1);
84 public NameSchemaType getSchemeName() {
85 return NameSchemaType.SINGLE;
89 public void output(PrintWriter out) {
90 out.print("<span class='sname'>");
91 out.print(HTMLEncoder.encodeHTML(singlePart.getValue()));
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
101 private static class WesternName extends SchemedName {
103 private NamePart[] firstNames;
105 private NamePart[] lastNames;
107 private NamePart[] suffixes;
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");
113 this.lastNames = lastName;
114 this.firstNames = firstName;
115 this.suffixes = suffixes;
119 public boolean matches(String text) {
120 String[] tokens = text.split(" ");
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)) {
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) {
144 currentlyMatchingLastNames = false;
145 } else if (tokens[userInputPos].equals(lastNames[referencePos].getValue())) {
152 if (tokens[userInputPos].equals(suffixes[referencePos].getValue())) {
160 if (userInputPos >= tokens.length) {
161 // all name parts are covered we're done here
167 private boolean tryMatchFirst(String[] tokens, int lastName) {
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])) {
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();
199 public void output(PrintWriter out) {
200 outputNameParts(out, "fname", firstNames, false);
201 outputNameParts(out, "lname", lastNames, true);
202 outputNameParts(out, "suffix", suffixes, true);
205 private void outputNameParts(PrintWriter out, String type, NamePart[] input, boolean leadingSpace) {
207 res = new StringBuilder();
208 appendArray(res, input);
209 if (res.length() > 0) {
210 res.deleteCharAt(res.length() - 1);
214 out.print("<span class='" + type + "'>");
215 out.print(HTMLEncoder.encodeHTML(res.toString()));
216 out.print("</span>");
220 private void appendArray(StringBuilder res, NamePart[] ps) {
221 for (int i = 0; i < ps.length; i++) {
222 res.append(ps[i].getValue());
228 public NameSchemaType getSchemeName() {
229 return NameSchemaType.WESTERN;
233 public String toAbbreviatedString() {
234 return firstNames[0].getValue() + " " + lastNames[0].getValue().charAt(0) + ".";
238 public String toInitialsString() {
240 String initals = getInitialByNamePart(firstNames, lastNames, suffixes);
251 * Only resolved lazily to resolve circular referencing with {@link User} on
252 * {@link User#getPreferredName()}. Resolved based on {@link #ownerId}.
256 private NamePart[] parts;
258 private SchemedName scheme;
261 * This name should not get verifed anymore and therefore not be displayed
262 * to the RA-Agent. This state is irrevocable.
264 private boolean deprecated;
266 private Name(GigiResultSet rs) {
267 ownerId = rs.getInt(1);
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();
274 NamePart[] dt = new NamePart[rs1.getRow()];
276 for (int i = 0; rs1.next(); i++) {
277 dt[i] = new NamePart(rs1);
280 scheme = detectScheme();
285 public Name(User u, NamePart... np) throws GigiApiException {
286 synchronized (Name.class) {
289 scheme = detectScheme();
290 if (scheme == null) {
291 throw new GigiApiException("Name particles don't match up for any known name scheme.");
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());
297 id = inserter.lastInsertId();
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());
312 private SchemedName detectScheme() {
313 if (parts.length == 1 && parts[0].getType() == NamePartType.SINGLE_NAME) {
314 return new SingleName(parts[0]);
320 for (NamePart p : parts) {
321 if (p.getType() == NamePartType.LAST_NAME) {
325 } else if (stage != 1) {
328 } else if (p.getType() == NamePartType.FIRST_NAME) {
333 } else if (p.getType() == NamePartType.SUFFIX) {
337 } else if (stage != 2) {
345 if (firstCount == 0 || lastCount == 0) {
348 NamePart[] firstNames = new NamePart[firstCount];
349 NamePart[] lastNames = new NamePart[lastCount];
350 NamePart[] suffixes = new NamePart[suffixCount];
354 for (NamePart p : parts) {
355 if (p.getType() == NamePartType.FIRST_NAME) {
356 firstNames[fn++] = p;
357 } else if (p.getType() == NamePartType.SUFFIX) {
359 } else if (p.getType() == NamePartType.LAST_NAME) {
364 return new WesternName(firstNames, lastNames, suffixes);
368 * Outputs an HTML variant suitable for locations where special UI features
369 * should indicate the different Name Parts.
372 public void output(PrintWriter out, Language l, Map<String, Object> vars) {
373 out.print("<span class=\"names\">");
375 out.print("</span>");
379 * Tests, if this name fits into the given string.
382 * the name to test against
383 * @return true, iff this name matches.
385 public boolean matches(String text) {
386 if ( !text.equals(text.trim())) {
389 return scheme.matches(text);
393 public String toString() {
394 return scheme.toPreferredString();
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.".)
402 * @return the short form of the name
404 public String toAbbreviatedString() {
405 return scheme.toAbbreviatedString();
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
413 * @return the short form of the name
415 public String toInitialsString() {
416 return scheme.toInitialsString();
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());
423 GigiResultSet rs = query.executeQuery();
427 points = rs.getInt(1);
439 private static ObjectCache<Name> cache = new ObjectCache<>();
441 public synchronized static Name getById(int id) {
442 Name cacheRes = cache.get(id);
443 if (cacheRes != null) {
447 try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `uid`, `id`, `deprecated` FROM `names` WHERE `deleted` IS NULL AND `id` = ?")) {
449 GigiResultSet rs = ps.executeQuery();
454 Name c = new Name(rs);
460 public NamePart[] getParts() {
464 public void remove() {
465 synchronized (Name.class) {
467 try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `names` SET `deleted` = now() WHERE `id`=?")) {
474 public synchronized void deprecate() {
476 try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `names` SET `deprecated`=now() WHERE `id`=?")) {
482 public boolean isDeprecated() {
486 public synchronized User getOwner() {
488 owner = User.getById(ownerId);
493 private static String getInitialByNamePart(NamePart[]... npa) {
494 StringBuilder initals = new StringBuilder();
495 for (NamePart[] np : npa) {
496 initals.append(getInitialByNamePart(np));
498 return initals.toString();
501 private static String getInitialByNamePart(NamePart[] np) {
502 StringBuilder initals = new StringBuilder();
503 for (NamePart p : np) {
504 switch (p.getValue()) {
509 initals.append(p.getValue().substring(0, 1).toUpperCase());
513 return initals.toString();