1 package org.cacert.gigi.dbObjects;
3 import java.io.PrintWriter;
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;
14 public class Name implements Outputable, IdCachable {
16 private abstract static class SchemedName {
19 * @see Name#matches(String)
21 public abstract boolean matches(String text);
23 public abstract String toPreferredString();
26 * @see Name#toAbbreviatedString()
28 public abstract String toAbbreviatedString();
30 public abstract String getSchemeName();
33 * @see Name#output(PrintWriter, Language, Map)
35 public abstract void output(PrintWriter out);
39 private static class SingleName extends SchemedName {
41 private NamePart singlePart;
43 public SingleName(NamePart singlePart) {
44 this.singlePart = singlePart;
48 public boolean matches(String text) {
49 return text.equals(singlePart.getValue());
53 public String toPreferredString() {
54 return singlePart.getValue();
58 public String toAbbreviatedString() {
59 return singlePart.getValue();
63 public String getSchemeName() {
68 public void output(PrintWriter out) {
69 out.print("<span class='sname'>");
70 out.print(HTMLEncoder.encodeHTML(singlePart.getValue()));
71 out.println("</span>");
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
80 private static class WesternName extends SchemedName {
82 private NamePart[] firstNames;
84 private NamePart[] lastNames;
86 private NamePart[] suffixes;
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");
92 this.lastNames = lastName;
93 this.firstNames = firstName;
94 this.suffixes = suffixes;
98 public boolean matches(String text) {
99 String[] tokens = text.split(" ");
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)) {
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) {
123 currentlyMatchingLastNames = false;
124 } else if (tokens[userInputPos].equals(lastNames[referencePos].getValue())) {
131 if (tokens[userInputPos].equals(suffixes[referencePos].getValue())) {
139 if (userInputPos >= tokens.length) {
140 // all name parts are covered we're done here
146 private boolean tryMatchFirst(String[] tokens, int lastName) {
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])) {
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();
178 public void output(PrintWriter out) {
179 outputNameParts(out, "fname", firstNames);
180 outputNameParts(out, "lname", lastNames);
181 outputNameParts(out, "suffix", suffixes);
184 private void outputNameParts(PrintWriter out, String type, NamePart[] input) {
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>");
196 private void appendArray(StringBuilder res, NamePart[] ps) {
197 for (int i = 0; i < ps.length; i++) {
198 res.append(ps[i].getValue());
204 public String getSchemeName() {
209 public String toAbbreviatedString() {
210 return firstNames[0].getValue() + " " + lastNames[0].getValue().charAt(0) + ".";
219 * Only resolved lazily to resolve circular referencing with {@link User} on
220 * {@link User#getPreferredName()}. Resolved based on {@link #ownerId}.
224 private NamePart[] parts;
226 private SchemedName scheme;
229 * This name should not get verifed anymore and therefore not be displayed
230 * to the RA-Agent. This state is irrevocable.
232 private boolean deprecated;
234 private Name(GigiResultSet rs) {
235 ownerId = rs.getInt(1);
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();
242 NamePart[] dt = new NamePart[rs1.getRow()];
244 for (int i = 0; rs1.next(); i++) {
245 dt[i] = new NamePart(rs1);
248 scheme = detectScheme();
253 public Name(User u, NamePart... np) throws GigiApiException {
254 synchronized (Name.class) {
257 scheme = detectScheme();
258 if (scheme == null) {
259 throw new GigiApiException("Name particles don't match up for any known name scheme.");
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());
265 id = inserter.lastInsertId();
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());
280 private SchemedName detectScheme() {
281 if (parts.length == 1 && parts[0].getType() == NamePartType.SINGLE_NAME) {
282 return new SingleName(parts[0]);
288 for (NamePart p : parts) {
289 if (p.getType() == NamePartType.LAST_NAME) {
293 } else if (stage != 1) {
296 } else if (p.getType() == NamePartType.FIRST_NAME) {
301 } else if (p.getType() == NamePartType.SUFFIX) {
305 } else if (stage != 2) {
313 if (firstCount == 0 || lastCount == 0) {
316 NamePart[] firstNames = new NamePart[firstCount];
317 NamePart[] lastNames = new NamePart[lastCount];
318 NamePart[] suffixes = new NamePart[suffixCount];
322 for (NamePart p : parts) {
323 if (p.getType() == NamePartType.FIRST_NAME) {
324 firstNames[fn++] = p;
325 } else if (p.getType() == NamePartType.SUFFIX) {
327 } else if (p.getType() == NamePartType.LAST_NAME) {
332 return new WesternName(firstNames, lastNames, suffixes);
336 * Outputs an HTML variant suitable for locations where special UI features
337 * should indicate the different Name Parts.
340 public void output(PrintWriter out, Language l, Map<String, Object> vars) {
341 out.print("<span class=\"names\">");
343 out.print("</span> ");
347 * Tests, if this name fits into the given string.
350 * the name to test against
351 * @return true, iff this name matches.
353 public boolean matches(String text) {
354 if ( !text.equals(text.trim())) {
357 return scheme.matches(text);
361 public String toString() {
362 return scheme.toPreferredString();
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.".)
370 * @return the short form of the name
372 public String toAbbreviatedString() {
373 return scheme.toAbbreviatedString();
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());
380 GigiResultSet rs = query.executeQuery();
384 points = rs.getInt(1);
396 private static ObjectCache<Name> cache = new ObjectCache<>();
398 public synchronized static Name getById(int id) {
399 Name cacheRes = cache.get(id);
400 if (cacheRes != null) {
404 try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `uid`, `id`, `deprecated` FROM `names` WHERE `deleted` IS NULL AND `id` = ?")) {
406 GigiResultSet rs = ps.executeQuery();
411 Name c = new Name(rs);
417 public NamePart[] getParts() {
421 public void remove() {
422 synchronized (Name.class) {
424 try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `names` SET `deleted` = now() WHERE `id`=?")) {
431 public synchronized void deprecate() {
433 try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `names` SET `deprecated`=now() WHERE `id`=?")) {
439 public boolean isDeprecated() {
443 public synchronized User getOwner() {
445 owner = User.getById(ownerId);