src/share/classes/com/sun/tools/doclets/internal/toolkit/util/VisibleMemberMap.java

Tue, 09 Oct 2012 19:10:00 -0700

author
jjg
date
Tue, 09 Oct 2012 19:10:00 -0700
changeset 1357
c75be5bc5283
parent 554
9d9f26857129
child 1358
fc123bdeddb8
permissions
-rw-r--r--

8000663: clean up langtools imports
Reviewed-by: darcy

duke@1 1 /*
jjg@1357 2 * Copyright (c) 1999, 2012, Oracle and/or its affiliates. All rights reserved.
duke@1 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
duke@1 4 *
duke@1 5 * This code is free software; you can redistribute it and/or modify it
duke@1 6 * under the terms of the GNU General Public License version 2 only, as
ohair@554 7 * published by the Free Software Foundation. Oracle designates this
duke@1 8 * particular file as subject to the "Classpath" exception as provided
ohair@554 9 * by Oracle in the LICENSE file that accompanied this code.
duke@1 10 *
duke@1 11 * This code is distributed in the hope that it will be useful, but WITHOUT
duke@1 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
duke@1 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
duke@1 14 * version 2 for more details (a copy is included in the LICENSE file that
duke@1 15 * accompanied this code).
duke@1 16 *
duke@1 17 * You should have received a copy of the GNU General Public License version
duke@1 18 * 2 along with this work; if not, write to the Free Software Foundation,
duke@1 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
duke@1 20 *
ohair@554 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
ohair@554 22 * or visit www.oracle.com if you need additional information or have any
ohair@554 23 * questions.
duke@1 24 */
duke@1 25
duke@1 26 package com.sun.tools.doclets.internal.toolkit.util;
duke@1 27
jjg@1357 28 import java.util.*;
jjg@1357 29
duke@1 30 import com.sun.javadoc.*;
duke@1 31 import com.sun.tools.doclets.internal.toolkit.*;
duke@1 32
duke@1 33 /**
duke@1 34 * A data structure that encapsulates the visible members of a particular
duke@1 35 * type for a given class tree. To use this data structor, you must specify
duke@1 36 * the type of member you are interested in (nested class, field, constructor
duke@1 37 * or method) and the leaf of the class tree. The data structure will map
duke@1 38 * all visible members in the leaf and classes above the leaf in the tree.
duke@1 39 *
duke@1 40 * This code is not part of an API.
duke@1 41 * It is implementation that is subject to change.
duke@1 42 * Do not use it as an API
duke@1 43 *
duke@1 44 * @author Atul M Dambalkar
duke@1 45 * @author Jamie Ho (rewrite)
duke@1 46 */
duke@1 47 public class VisibleMemberMap {
duke@1 48
duke@1 49 private boolean noVisibleMembers = true;
duke@1 50
duke@1 51 public static final int INNERCLASSES = 0;
duke@1 52 public static final int ENUM_CONSTANTS = 1;
duke@1 53 public static final int FIELDS = 2;
duke@1 54 public static final int CONSTRUCTORS = 3;
duke@1 55 public static final int METHODS = 4;
duke@1 56 public static final int ANNOTATION_TYPE_MEMBER_OPTIONAL = 5;
duke@1 57 public static final int ANNOTATION_TYPE_MEMBER_REQUIRED = 6;
duke@1 58
duke@1 59 /**
duke@1 60 * The total number of member types is {@value}.
duke@1 61 */
duke@1 62 public static final int NUM_MEMBER_TYPES = 7;
duke@1 63
duke@1 64 public static final String STARTLEVEL = "start";
duke@1 65
duke@1 66 /**
duke@1 67 * List of ClassDoc objects for which ClassMembers objects are built.
duke@1 68 */
jjg@74 69 private final List<ClassDoc> visibleClasses = new ArrayList<ClassDoc>();
duke@1 70
duke@1 71 /**
duke@1 72 * Map for each member name on to a map which contains members with same
duke@1 73 * name-signature. The mapped map will contain mapping for each MemberDoc
duke@1 74 * onto it's respecive level string.
duke@1 75 */
jjg@74 76 private final Map<Object,Map<ProgramElementDoc,String>> memberNameMap = new HashMap<Object,Map<ProgramElementDoc,String>>();
duke@1 77
duke@1 78 /**
duke@1 79 * Map of class and it's ClassMembers object.
duke@1 80 */
jjg@74 81 private final Map<ClassDoc,ClassMembers> classMap = new HashMap<ClassDoc,ClassMembers>();
duke@1 82
duke@1 83 /**
duke@1 84 * Type whose visible members are requested. This is the leaf of
duke@1 85 * the class tree being mapped.
duke@1 86 */
duke@1 87 private final ClassDoc classdoc;
duke@1 88
duke@1 89 /**
duke@1 90 * Member kind: InnerClasses/Fields/Methods?
duke@1 91 */
duke@1 92 private final int kind;
duke@1 93
duke@1 94 /**
duke@1 95 * Deprected members should be excluded or not?
duke@1 96 */
duke@1 97 private final boolean nodepr;
duke@1 98
duke@1 99 /**
duke@1 100 * Construct a VisibleMemberMap of the given type for the given
duke@1 101 * class. If nodepr is true, exclude the deprecated members from
duke@1 102 * the map.
duke@1 103 *
duke@1 104 * @param classdoc the class whose members are being mapped.
duke@1 105 * @param kind the kind of member that is being mapped.
duke@1 106 * @param nodepr if true, exclude the deprecated members from the map.
duke@1 107 */
duke@1 108 public VisibleMemberMap(ClassDoc classdoc, int kind, boolean nodepr) {
duke@1 109 this.classdoc = classdoc;
duke@1 110 this.nodepr = nodepr;
duke@1 111 this.kind = kind;
duke@1 112 new ClassMembers(classdoc, STARTLEVEL).build();
duke@1 113 }
duke@1 114
duke@1 115 /**
duke@1 116 * Return the list of visible classes in this map.
duke@1 117 *
duke@1 118 * @return the list of visible classes in this map.
duke@1 119 */
mcimadamore@184 120 public List<ClassDoc> getVisibleClassesList() {
duke@1 121 sort(visibleClasses);
duke@1 122 return visibleClasses;
duke@1 123 }
duke@1 124
duke@1 125 /**
duke@1 126 * Return the package private members inherited by the class. Only return
duke@1 127 * if parent is package private and not documented.
duke@1 128 *
duke@1 129 * @param configuation the current configuration of the doclet.
duke@1 130 * @return the package private members inherited by the class.
duke@1 131 */
jjg@74 132 private List<ProgramElementDoc> getInheritedPackagePrivateMethods(Configuration configuration) {
jjg@74 133 List<ProgramElementDoc> results = new ArrayList<ProgramElementDoc>();
mcimadamore@184 134 for (Iterator<ClassDoc> iter = visibleClasses.iterator(); iter.hasNext(); ) {
mcimadamore@184 135 ClassDoc currentClass = iter.next();
duke@1 136 if (currentClass != classdoc &&
duke@1 137 currentClass.isPackagePrivate() &&
duke@1 138 !Util.isLinkable(currentClass, configuration)) {
duke@1 139 // Document these members in the child class because
duke@1 140 // the parent is inaccessible.
duke@1 141 results.addAll(getMembersFor(currentClass));
duke@1 142 }
duke@1 143 }
duke@1 144 return results;
duke@1 145 }
duke@1 146
duke@1 147 /**
duke@1 148 * Return the visible members of the class being mapped. Also append at the
duke@1 149 * end of the list members that are inherited by inaccessible parents. We
duke@1 150 * document these members in the child because the parent is not documented.
duke@1 151 *
duke@1 152 * @param configuation the current configuration of the doclet.
duke@1 153 */
jjg@74 154 public List<ProgramElementDoc> getLeafClassMembers(Configuration configuration) {
jjg@74 155 List<ProgramElementDoc> result = getMembersFor(classdoc);
duke@1 156 result.addAll(getInheritedPackagePrivateMethods(configuration));
duke@1 157 return result;
duke@1 158 }
duke@1 159
duke@1 160 /**
duke@1 161 * Retrn the list of members for the given class.
duke@1 162 *
duke@1 163 * @param cd the class to retrieve the list of visible members for.
duke@1 164 *
duke@1 165 * @return the list of members for the given class.
duke@1 166 */
jjg@74 167 public List<ProgramElementDoc> getMembersFor(ClassDoc cd) {
jjg@74 168 ClassMembers clmembers = classMap.get(cd);
duke@1 169 if (clmembers == null) {
jjg@74 170 return new ArrayList<ProgramElementDoc>();
duke@1 171 }
duke@1 172 return clmembers.getMembers();
duke@1 173 }
duke@1 174
duke@1 175 /**
duke@1 176 * Sort the given mixed list of classes and interfaces to a list of
duke@1 177 * classes followed by interfaces traversed. Don't sort alphabetically.
duke@1 178 */
jjg@74 179 private void sort(List<ClassDoc> list) {
jjg@74 180 List<ClassDoc> classes = new ArrayList<ClassDoc>();
jjg@74 181 List<ClassDoc> interfaces = new ArrayList<ClassDoc>();
duke@1 182 for (int i = 0; i < list.size(); i++) {
jjg@74 183 ClassDoc cd = list.get(i);
duke@1 184 if (cd.isClass()) {
duke@1 185 classes.add(cd);
duke@1 186 } else {
duke@1 187 interfaces.add(cd);
duke@1 188 }
duke@1 189 }
duke@1 190 list.clear();
duke@1 191 list.addAll(classes);
duke@1 192 list.addAll(interfaces);
duke@1 193 }
duke@1 194
jjg@74 195 private void fillMemberLevelMap(List<ProgramElementDoc> list, String level) {
duke@1 196 for (int i = 0; i < list.size(); i++) {
jjg@74 197 Object key = getMemberKey(list.get(i));
jjg@74 198 Map<ProgramElementDoc,String> memberLevelMap = memberNameMap.get(key);
duke@1 199 if (memberLevelMap == null) {
jjg@74 200 memberLevelMap = new HashMap<ProgramElementDoc,String>();
duke@1 201 memberNameMap.put(key, memberLevelMap);
duke@1 202 }
duke@1 203 memberLevelMap.put(list.get(i), level);
duke@1 204 }
duke@1 205 }
duke@1 206
mcimadamore@184 207 private void purgeMemberLevelMap(List<ProgramElementDoc> list, String level) {
duke@1 208 for (int i = 0; i < list.size(); i++) {
mcimadamore@184 209 Object key = getMemberKey(list.get(i));
mcimadamore@184 210 Map<ProgramElementDoc, String> memberLevelMap = memberNameMap.get(key);
duke@1 211 if (level.equals(memberLevelMap.get(list.get(i))))
duke@1 212 memberLevelMap.remove(list.get(i));
duke@1 213 }
duke@1 214 }
duke@1 215
duke@1 216 /**
duke@1 217 * Represents a class member. We should be able to just use a
duke@1 218 * ProgramElementDoc instead of this class, but that doesn't take
duke@1 219 * type variables in consideration when comparing.
duke@1 220 */
duke@1 221 private class ClassMember {
jjg@74 222 private Set<ProgramElementDoc> members;
duke@1 223
duke@1 224 public ClassMember(ProgramElementDoc programElementDoc) {
jjg@74 225 members = new HashSet<ProgramElementDoc>();
duke@1 226 members.add(programElementDoc);
duke@1 227 }
duke@1 228
duke@1 229 public void addMember(ProgramElementDoc programElementDoc) {
duke@1 230 members.add(programElementDoc);
duke@1 231 }
duke@1 232
duke@1 233 public boolean isEqual(MethodDoc member) {
mcimadamore@184 234 for (Iterator<ProgramElementDoc> iter = members.iterator(); iter.hasNext(); ) {
duke@1 235 MethodDoc member2 = (MethodDoc) iter.next();
duke@1 236 if (Util.executableMembersEqual(member, member2)) {
duke@1 237 members.add(member);
duke@1 238 return true;
duke@1 239 }
duke@1 240 }
duke@1 241 return false;
duke@1 242 }
duke@1 243 }
duke@1 244
duke@1 245 /**
duke@1 246 * A data structure that represents the class members for
duke@1 247 * a visible class.
duke@1 248 */
duke@1 249 private class ClassMembers {
duke@1 250
duke@1 251 /**
duke@1 252 * The mapping class, whose inherited members are put in the
duke@1 253 * {@link #members} list.
duke@1 254 */
duke@1 255 private ClassDoc mappingClass;
duke@1 256
duke@1 257 /**
duke@1 258 * List of inherited members from the mapping class.
duke@1 259 */
jjg@74 260 private List<ProgramElementDoc> members = new ArrayList<ProgramElementDoc>();
duke@1 261
duke@1 262 /**
duke@1 263 * Level/Depth of inheritance.
duke@1 264 */
duke@1 265 private String level;
duke@1 266
duke@1 267 /**
duke@1 268 * Return list of inherited members from mapping class.
duke@1 269 *
duke@1 270 * @return List Inherited members.
duke@1 271 */
jjg@74 272 public List<ProgramElementDoc> getMembers() {
duke@1 273 return members;
duke@1 274 }
duke@1 275
duke@1 276 private ClassMembers(ClassDoc mappingClass, String level) {
duke@1 277 this.mappingClass = mappingClass;
duke@1 278 this.level = level;
duke@1 279 if (classMap.containsKey(mappingClass) &&
jjg@74 280 level.startsWith(classMap.get(mappingClass).level)) {
duke@1 281 //Remove lower level class so that it can be replaced with
duke@1 282 //same class found at higher level.
duke@1 283 purgeMemberLevelMap(getClassMembers(mappingClass, false),
jjg@74 284 classMap.get(mappingClass).level);
duke@1 285 classMap.remove(mappingClass);
duke@1 286 visibleClasses.remove(mappingClass);
duke@1 287 }
duke@1 288 if (!classMap.containsKey(mappingClass)) {
duke@1 289 classMap.put(mappingClass, this);
duke@1 290 visibleClasses.add(mappingClass);
duke@1 291 }
duke@1 292
duke@1 293 }
duke@1 294
duke@1 295 private void build() {
duke@1 296 if (kind == CONSTRUCTORS) {
duke@1 297 addMembers(mappingClass);
duke@1 298 } else {
duke@1 299 mapClass();
duke@1 300 }
duke@1 301 }
duke@1 302
duke@1 303 private void mapClass() {
duke@1 304 addMembers(mappingClass);
duke@1 305 ClassDoc[] interfaces = mappingClass.interfaces();
duke@1 306 for (int i = 0; i < interfaces.length; i++) {
duke@1 307 String locallevel = level + 1;
duke@1 308 ClassMembers cm = new ClassMembers(interfaces[i], locallevel);
duke@1 309 cm.mapClass();
duke@1 310 }
duke@1 311 if (mappingClass.isClass()) {
duke@1 312 ClassDoc superclass = mappingClass.superclass();
duke@1 313 if (!(superclass == null || mappingClass.equals(superclass))) {
duke@1 314 ClassMembers cm = new ClassMembers(superclass,
duke@1 315 level + "c");
duke@1 316 cm.mapClass();
duke@1 317 }
duke@1 318 }
duke@1 319 }
duke@1 320
duke@1 321 /**
duke@1 322 * Get all the valid members from the mapping class. Get the list of
duke@1 323 * members for the class to be included into(ctii), also get the level
duke@1 324 * string for ctii. If mapping class member is not already in the
duke@1 325 * inherited member list and if it is visible in the ctii and not
duke@1 326 * overridden, put such a member in the inherited member list.
duke@1 327 * Adjust member-level-map, class-map.
duke@1 328 */
duke@1 329 private void addMembers(ClassDoc fromClass) {
jjg@74 330 List<ProgramElementDoc> cdmembers = getClassMembers(fromClass, true);
jjg@74 331 List<ProgramElementDoc> incllist = new ArrayList<ProgramElementDoc>();
duke@1 332 for (int i = 0; i < cdmembers.size(); i++) {
jjg@74 333 ProgramElementDoc pgmelem = cdmembers.get(i);
duke@1 334 if (!found(members, pgmelem) &&
duke@1 335 memberIsVisible(pgmelem) &&
duke@1 336 !isOverridden(pgmelem, level)) {
duke@1 337 incllist.add(pgmelem);
duke@1 338 }
duke@1 339 }
duke@1 340 if (incllist.size() > 0) {
duke@1 341 noVisibleMembers = false;
duke@1 342 }
duke@1 343 members.addAll(incllist);
duke@1 344 fillMemberLevelMap(getClassMembers(fromClass, false), level);
duke@1 345 }
duke@1 346
duke@1 347 /**
duke@1 348 * Is given doc item visible in given classdoc in terms fo inheritance?
duke@1 349 * The given doc item is visible in the given classdoc if it is public
duke@1 350 * or protected and if it is package-private if it's containing class
duke@1 351 * is in the same package as the given classdoc.
duke@1 352 */
duke@1 353 private boolean memberIsVisible(ProgramElementDoc pgmdoc) {
duke@1 354 if (pgmdoc.containingClass().equals(classdoc)) {
duke@1 355 //Member is in class that we are finding visible members for.
duke@1 356 //Of course it is visible.
duke@1 357 return true;
duke@1 358 } else if (pgmdoc.isPrivate()) {
duke@1 359 //Member is in super class or implemented interface.
duke@1 360 //Private, so not inherited.
duke@1 361 return false;
duke@1 362 } else if (pgmdoc.isPackagePrivate()) {
duke@1 363 //Member is package private. Only return true if its class is in
duke@1 364 //same package.
duke@1 365 return pgmdoc.containingClass().containingPackage().equals(
duke@1 366 classdoc.containingPackage());
duke@1 367 } else {
duke@1 368 //Public members are always inherited.
duke@1 369 return true;
duke@1 370 }
duke@1 371 }
duke@1 372
duke@1 373 /**
duke@1 374 * Return all available class members.
duke@1 375 */
jjg@74 376 private List<ProgramElementDoc> getClassMembers(ClassDoc cd, boolean filter) {
duke@1 377 if (cd.isEnum() && kind == CONSTRUCTORS) {
duke@1 378 //If any of these rules are hit, return empty array because
duke@1 379 //we don't document these members ever.
duke@1 380 return Arrays.asList(new ProgramElementDoc[] {});
duke@1 381 }
duke@1 382 ProgramElementDoc[] members = null;
duke@1 383 switch (kind) {
duke@1 384 case ANNOTATION_TYPE_MEMBER_OPTIONAL:
duke@1 385 members = cd.isAnnotationType() ?
duke@1 386 filter((AnnotationTypeDoc) cd, false) :
duke@1 387 new AnnotationTypeElementDoc[] {};
duke@1 388 break;
duke@1 389 case ANNOTATION_TYPE_MEMBER_REQUIRED:
duke@1 390 members = cd.isAnnotationType() ?
duke@1 391 filter((AnnotationTypeDoc) cd, true) :
duke@1 392 new AnnotationTypeElementDoc[] {};
duke@1 393 break;
duke@1 394 case INNERCLASSES:
duke@1 395 members = cd.innerClasses(filter);
duke@1 396 break;
duke@1 397 case ENUM_CONSTANTS:
duke@1 398 members = cd.enumConstants();
duke@1 399 break;
duke@1 400 case FIELDS:
duke@1 401 members = cd.fields(filter);
duke@1 402 break;
duke@1 403 case CONSTRUCTORS:
duke@1 404 members = cd.constructors();
duke@1 405 break;
duke@1 406 case METHODS:
duke@1 407 members = cd.methods(filter);
duke@1 408 break;
duke@1 409 default:
duke@1 410 members = new ProgramElementDoc[0];
duke@1 411 }
duke@1 412 if (nodepr) {
duke@1 413 return Util.excludeDeprecatedMembersAsList(members);
duke@1 414 }
duke@1 415 return Arrays.asList(members);
duke@1 416 }
duke@1 417
duke@1 418 /**
duke@1 419 * Filter the annotation type members and return either the required
duke@1 420 * members or the optional members, depending on the value of the
duke@1 421 * required parameter.
duke@1 422 *
duke@1 423 * @param doc The annotation type to process.
duke@1 424 * @param required
duke@1 425 * @return the annotation type members and return either the required
duke@1 426 * members or the optional members, depending on the value of the
duke@1 427 * required parameter.
duke@1 428 */
duke@1 429 private AnnotationTypeElementDoc[] filter(AnnotationTypeDoc doc,
duke@1 430 boolean required) {
jjg@74 431 AnnotationTypeElementDoc[] members = doc.elements();
jjg@74 432 List<AnnotationTypeElementDoc> targetMembers = new ArrayList<AnnotationTypeElementDoc>();
duke@1 433 for (int i = 0; i < members.length; i++) {
duke@1 434 if ((required && members[i].defaultValue() == null) ||
duke@1 435 ((!required) && members[i].defaultValue() != null)){
duke@1 436 targetMembers.add(members[i]);
duke@1 437 }
duke@1 438 }
jjg@74 439 return targetMembers.toArray(new AnnotationTypeElementDoc[]{});
duke@1 440 }
duke@1 441
mcimadamore@184 442 private boolean found(List<ProgramElementDoc> list, ProgramElementDoc elem) {
duke@1 443 for (int i = 0; i < list.size(); i++) {
mcimadamore@184 444 ProgramElementDoc pgmelem = list.get(i);
duke@1 445 if (Util.matches(pgmelem, elem)) {
duke@1 446 return true;
duke@1 447 }
duke@1 448 }
duke@1 449 return false;
duke@1 450 }
duke@1 451
duke@1 452
duke@1 453 /**
duke@1 454 * Is member overridden? The member is overridden if it is found in the
duke@1 455 * same level hierarchy e.g. member at level "11" overrides member at
duke@1 456 * level "111".
duke@1 457 */
duke@1 458 private boolean isOverridden(ProgramElementDoc pgmdoc, String level) {
mcimadamore@184 459 Map<?,String> memberLevelMap = (Map<?,String>) memberNameMap.get(getMemberKey(pgmdoc));
duke@1 460 if (memberLevelMap == null)
duke@1 461 return false;
duke@1 462 String mappedlevel = null;
mcimadamore@184 463 Iterator<String> iterator = memberLevelMap.values().iterator();
duke@1 464 while (iterator.hasNext()) {
mcimadamore@184 465 mappedlevel = iterator.next();
duke@1 466 if (mappedlevel.equals(STARTLEVEL) ||
duke@1 467 (level.startsWith(mappedlevel) &&
duke@1 468 !level.equals(mappedlevel))) {
duke@1 469 return true;
duke@1 470 }
duke@1 471 }
duke@1 472 return false;
duke@1 473 }
duke@1 474 }
duke@1 475
duke@1 476 /**
duke@1 477 * Return true if this map has no visible members.
duke@1 478 *
duke@1 479 * @return true if this map has no visible members.
duke@1 480 */
duke@1 481 public boolean noVisibleMembers() {
duke@1 482 return noVisibleMembers;
duke@1 483 }
duke@1 484
duke@1 485 private ClassMember getClassMember(MethodDoc member) {
mcimadamore@184 486 for (Iterator<?> iter = memberNameMap.keySet().iterator(); iter.hasNext();) {
duke@1 487 Object key = iter.next();
duke@1 488 if (key instanceof String) {
duke@1 489 continue;
duke@1 490 } else if (((ClassMember) key).isEqual(member)) {
duke@1 491 return (ClassMember) key;
duke@1 492 }
duke@1 493 }
duke@1 494 return new ClassMember(member);
duke@1 495 }
duke@1 496
duke@1 497 /**
duke@1 498 * Return the key to the member map for the given member.
duke@1 499 */
duke@1 500 private Object getMemberKey(ProgramElementDoc doc) {
duke@1 501 if (doc.isConstructor()) {
duke@1 502 return doc.name() + ((ExecutableMemberDoc)doc).signature();
duke@1 503 } else if (doc.isMethod()) {
duke@1 504 return getClassMember((MethodDoc) doc);
duke@1 505 } else if (doc.isField() || doc.isEnumConstant() || doc.isAnnotationTypeElement()) {
duke@1 506 return doc.name();
duke@1 507 } else { // it's a class or interface
duke@1 508 String classOrIntName = doc.name();
duke@1 509 //Strip off the containing class name because we only want the member name.
duke@1 510 classOrIntName = classOrIntName.indexOf('.') != 0 ? classOrIntName.substring(classOrIntName.lastIndexOf('.'), classOrIntName.length()) : classOrIntName;
duke@1 511 return "clint" + classOrIntName;
duke@1 512 }
duke@1 513 }
duke@1 514 }

mercurial