1 package club.wpia.gigi.dbObjects;
3 import java.io.PrintWriter;
4 import java.text.SimpleDateFormat;
5 import java.util.Calendar;
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;
19 public class Name implements Outputable, IdCachable {
21 public static enum NameSchemaType implements DBEnum {
25 public String getDBName() {
26 return toString().toLowerCase();
31 private abstract static class SchemedName {
34 * @see Name#matches(String)
36 public abstract boolean matches(String text);
38 public abstract String toPreferredString();
41 * @see Name#toAbbreviatedString()
43 public abstract String toAbbreviatedString();
45 public abstract NameSchemaType getSchemeName();
48 * @see Name#output(PrintWriter, Language, Map)
50 public abstract void output(PrintWriter out);
53 * @see Name#toInitialsString()
55 public abstract String toInitialsString();
59 private static class SingleName extends SchemedName {
61 private NamePart singlePart;
63 public SingleName(NamePart singlePart) {
64 this.singlePart = singlePart;
68 public boolean matches(String text) {
69 return text.equals(singlePart.getValue());
73 public String toPreferredString() {
74 return singlePart.getValue();
78 public String toAbbreviatedString() {
79 return singlePart.getValue();
83 public String toInitialsString() {
84 return singlePart.getValue().substring(0, 1);
88 public NameSchemaType getSchemeName() {
89 return NameSchemaType.SINGLE;
93 public void output(PrintWriter out) {
94 out.print("<span class='sname'>");
95 out.print(HTMLEncoder.encodeHTML(singlePart.getValue()));
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
105 private static class WesternName extends SchemedName {
107 private NamePart[] firstNames;
109 private NamePart[] lastNames;
111 private NamePart[] suffixes;
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");
117 this.lastNames = lastName;
118 this.firstNames = firstName;
119 this.suffixes = suffixes;
123 public boolean matches(String text) {
124 String[] tokens = text.split(" ");
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)) {
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) {
148 currentlyMatchingLastNames = false;
149 } else if (tokens[userInputPos].equals(lastNames[referencePos].getValue())) {
156 if (tokens[userInputPos].equals(suffixes[referencePos].getValue())) {
164 if (userInputPos >= tokens.length) {
165 // all name parts are covered we're done here
171 private boolean tryMatchFirst(String[] tokens, int lastName) {
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])) {
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();
203 public void output(PrintWriter out) {
204 outputNameParts(out, "fname", firstNames, false);
205 outputNameParts(out, "lname", lastNames, true);
206 outputNameParts(out, "suffix", suffixes, true);
209 private void outputNameParts(PrintWriter out, String type, NamePart[] input, boolean leadingSpace) {
211 res = new StringBuilder();
212 appendArray(res, input);
213 if (res.length() > 0) {
214 res.deleteCharAt(res.length() - 1);
218 out.print("<span class='" + type + "'>");
219 out.print(HTMLEncoder.encodeHTML(res.toString()));
220 out.print("</span>");
224 private void appendArray(StringBuilder res, NamePart[] ps) {
225 for (int i = 0; i < ps.length; i++) {
226 res.append(ps[i].getValue());
232 public NameSchemaType getSchemeName() {
233 return NameSchemaType.WESTERN;
237 public String toAbbreviatedString() {
238 return firstNames[0].getValue() + " " + lastNames[0].getValue().charAt(0) + ".";
242 public String toInitialsString() {
244 String initals = getInitialByNamePart(firstNames, lastNames, suffixes);
255 * Only resolved lazily to resolve circular referencing with {@link User} on
256 * {@link User#getPreferredName()}. Resolved based on {@link #ownerId}.
260 private NamePart[] parts;
262 private SchemedName scheme;
265 * This name should not get verifed anymore and therefore not be displayed
266 * to the RA-Agent. This state is irrevocable.
268 private boolean deprecated;
270 private Name(GigiResultSet rs) {
271 ownerId = rs.getInt(1);
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();
278 NamePart[] dt = new NamePart[rs1.getRow()];
280 for (int i = 0; rs1.next(); i++) {
281 dt[i] = new NamePart(rs1);
284 scheme = detectScheme();
289 public Name(User u, NamePart... np) throws GigiApiException {
290 synchronized (Name.class) {
293 scheme = detectScheme();
294 if (scheme == null) {
295 throw new GigiApiException("Name particles don't match up for any known name scheme.");
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());
301 id = inserter.lastInsertId();
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());
316 private SchemedName detectScheme() {
317 if (parts.length == 1 && parts[0].getType() == NamePartType.SINGLE_NAME) {
318 return new SingleName(parts[0]);
324 for (NamePart p : parts) {
325 if (p.getType() == NamePartType.LAST_NAME) {
329 } else if (stage != 1) {
332 } else if (p.getType() == NamePartType.FIRST_NAME) {
337 } else if (p.getType() == NamePartType.SUFFIX) {
341 } else if (stage != 2) {
349 if (firstCount == 0 || lastCount == 0) {
352 NamePart[] firstNames = new NamePart[firstCount];
353 NamePart[] lastNames = new NamePart[lastCount];
354 NamePart[] suffixes = new NamePart[suffixCount];
358 for (NamePart p : parts) {
359 if (p.getType() == NamePartType.FIRST_NAME) {
360 firstNames[fn++] = p;
361 } else if (p.getType() == NamePartType.SUFFIX) {
363 } else if (p.getType() == NamePartType.LAST_NAME) {
368 return new WesternName(firstNames, lastNames, suffixes);
372 * Outputs an HTML variant suitable for locations where special UI features
373 * should indicate the different Name Parts.
376 public void output(PrintWriter out, Language l, Map<String, Object> vars) {
377 out.print("<span class=\"names\">");
379 out.print("</span>");
383 * Tests, if this name fits into the given string.
386 * the name to test against
387 * @return true, iff this name matches.
389 public boolean matches(String text) {
390 if ( !text.equals(text.trim())) {
393 return scheme.matches(text);
397 public String toString() {
398 return scheme.toPreferredString();
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.".)
406 * @return the short form of the name
408 public String toAbbreviatedString() {
409 return scheme.toAbbreviatedString();
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
417 * @return the short form of the name
419 public String toInitialsString() {
420 return scheme.toInitialsString();
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());
427 GigiResultSet rs = query.executeQuery();
431 points = rs.getInt(1);
443 private static ObjectCache<Name> cache = new ObjectCache<>();
445 public synchronized static Name getById(int id) {
446 Name cacheRes = cache.get(id);
447 if (cacheRes != null) {
451 try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `uid`, `id`, `deprecated` FROM `names` WHERE `deleted` IS NULL AND `id` = ?")) {
453 GigiResultSet rs = ps.executeQuery();
458 Name c = new Name(rs);
464 public NamePart[] getParts() {
468 public void remove() {
469 synchronized (Name.class) {
471 try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `names` SET `deleted` = now() WHERE `id`=?")) {
478 public synchronized void deprecate() {
480 try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `names` SET `deprecated`=now() WHERE `id`=?")) {
486 public boolean isDeprecated() {
490 public synchronized User getOwner() {
492 owner = User.getById(ownerId);
497 private static String getInitialByNamePart(NamePart[]... npa) {
498 StringBuilder initals = new StringBuilder();
499 for (NamePart[] np : npa) {
500 initals.append(getInitialByNamePart(np));
502 return initals.toString();
505 private static String getInitialByNamePart(NamePart[] np) {
506 StringBuilder initals = new StringBuilder();
507 for (NamePart p : np) {
508 switch (p.getValue()) {
513 initals.append(p.getValue().substring(0, 1).toUpperCase());
517 return initals.toString();
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();
532 if (rs.getInt(1) > 0) {