ICU-7395 Committing the exact same updates with r27933 (maint-4-4) to the trunk. API comparison with 4.4 and beyond will be done by the new tool code.
X-SVN-Rev: 27934
This commit is contained in:
parent
c1db9d5f37
commit
61aa8a5447
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -428,6 +428,7 @@ icu4j/tools/build/icu4j401.api.gz -text
|
||||
icu4j/tools/build/icu4j42.api.gz -text
|
||||
icu4j/tools/build/icu4j421.api.gz -text
|
||||
icu4j/tools/build/icu4j44.api.gz -text
|
||||
icu4j/tools/build/icu4j44.api2.gz -text
|
||||
icu4j/tools/build/manifest.stub -text
|
||||
icu4j/tools/misc/manifest.stub -text
|
||||
tools/multi/c/Makefile-c.inc -text
|
||||
|
@ -585,7 +585,7 @@
|
||||
</packageset>
|
||||
<doclet name="com.ibm.icu.dev.tool.docs.GatherAPIData" path="${icu4j.build-tools.jar}">
|
||||
<param name="-name" value="ICU4J ${icu4j.impl.version}"/>
|
||||
<param name="-output" value="${out.dir}/icu4j${api.report.version}.api"/>
|
||||
<param name="-output" value="${out.dir}/icu4j${api.report.version}.api2"/>
|
||||
<param name="-internal"/>
|
||||
<param name="-gzip"/>
|
||||
</doclet>
|
||||
@ -593,6 +593,49 @@
|
||||
</target>
|
||||
|
||||
<target name="apireport" depends="info, gatherapi" description="Run API report generator tool">
|
||||
<java classname="com.ibm.icu.dev.tool.docs.ReportAPI"
|
||||
classpath="${icu4j.build-tools.jar}"
|
||||
failonerror="true">
|
||||
<arg value="-old:" />
|
||||
<arg value="${icu4j.build-tools.dir}/icu4j${api.report.prev.version}.api2.gz" />
|
||||
<arg value="-new:" />
|
||||
<arg value="${out.dir}/icu4j${api.report.version}.api2.gz" />
|
||||
<arg value="-html" />
|
||||
<arg value="-internal" />
|
||||
<arg value="-out:" />
|
||||
<arg value="${out.dir}/icu4j_compare_${api.report.prev.version}_${api.report.version}.html" />
|
||||
</java>
|
||||
</target>
|
||||
|
||||
<target name="gatherapiOld" depends="info, build-tools" description="Run API database generator tool (Pre Java 5 style)">
|
||||
<mkdir dir="${out.dir}"/>
|
||||
<javadoc source="1.5">
|
||||
<packageset dir="${icu4j.core.dir}/src">
|
||||
<include name="com/ibm/icu/lang/**"/>
|
||||
<include name="com/ibm/icu/math/**"/>
|
||||
<include name="com/ibm/icu/text/**"/>
|
||||
<include name="com/ibm/icu/util/**"/>
|
||||
</packageset>
|
||||
<packageset dir="${icu4j.collate.dir}/src">
|
||||
<include name="com/ibm/icu/text/**"/>
|
||||
<include name="com/ibm/icu/util/**"/>
|
||||
</packageset>
|
||||
<packageset dir="${icu4j.charset.dir}/src">
|
||||
<include name="com/ibm/icu/charset/**"/>
|
||||
</packageset>
|
||||
<packageset dir="${icu4j.translit.dir}/src">
|
||||
<include name="com/ibm/icu/text/**"/>
|
||||
</packageset>
|
||||
<doclet name="com.ibm.icu.dev.tool.docs.GatherAPIDataOld" path="${icu4j.build-tools.jar}">
|
||||
<param name="-name" value="ICU4J ${icu4j.impl.version}"/>
|
||||
<param name="-output" value="${out.dir}/icu4j${api.report.version}.api"/>
|
||||
<param name="-internal"/>
|
||||
<param name="-gzip"/>
|
||||
</doclet>
|
||||
</javadoc>
|
||||
</target>
|
||||
|
||||
<target name="apireportOld" depends="info, gatherapiOld" description="Run API report generator tool (Pre Java 5 Style)">
|
||||
<java classname="com.ibm.icu.dev.tool.docs.ReportAPI"
|
||||
classpath="${icu4j.build-tools.jar}"
|
||||
failonerror="true">
|
||||
|
3
icu4j/tools/build/icu4j44.api2.gz
Normal file
3
icu4j/tools/build/icu4j44.api2.gz
Normal file
@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b6db852067ffdd486e8b114ec12babfd9b794c04e3a8136cf2a8abec8e764ec6
|
||||
size 35967
|
@ -29,7 +29,7 @@ public final class APIData {
|
||||
int version;
|
||||
String name;
|
||||
String base;
|
||||
TreeSet set;
|
||||
TreeSet<APIInfo> set;
|
||||
|
||||
static APIData read(BufferedReader br, boolean internal) {
|
||||
try {
|
||||
@ -37,7 +37,9 @@ public final class APIData {
|
||||
|
||||
data.version = Integer.parseInt(APIInfo.readToken(br)); // version
|
||||
if (data.version > APIInfo.VERSION) {
|
||||
throw new IllegalArgumentException("data version " + data.version + " is newer than current version (" + APIInfo.VERSION + ")");
|
||||
throw new IllegalArgumentException(
|
||||
"data version " + data.version
|
||||
+ " is newer than current version (" + APIInfo.VERSION + ")");
|
||||
}
|
||||
data.name = APIInfo.readToken(br);
|
||||
data.base = APIInfo.readToken(br); // base
|
||||
@ -93,8 +95,12 @@ public final class APIData {
|
||||
return read(new File(fileName), internal);
|
||||
}
|
||||
|
||||
private static final String[] stanames = { "draft", "stable", "deprecated", "obsolete", "internal" };
|
||||
private static final String[] catnames = { "classes", "fields", "constructors", "methods" };
|
||||
private static final String[] stanames = {
|
||||
"draft", "stable", "deprecated", "obsolete", "internal"
|
||||
};
|
||||
private static final String[] catnames = {
|
||||
"classes", "fields", "constructors", "methods"
|
||||
};
|
||||
|
||||
public void printStats(PrintWriter pw) {
|
||||
// classes, methods, fields
|
||||
|
@ -24,13 +24,16 @@ class APIInfo {
|
||||
|
||||
// public keys and values for queries on info
|
||||
|
||||
public static final int STA = 0, STA_DRAFT = 0, STA_STABLE = 1, STA_DEPRECATED = 2, STA_OBSOLETE = 3, STA_INTERNAL = 4;
|
||||
public static final int VIS = 1, VIS_PACKAGE = 0, VIS_PUBLIC= 1, VIS_PROTECTED = 2, VIS_PRIVATE = 3;
|
||||
public static final int STA = 0, STA_DRAFT = 0, STA_STABLE = 1, STA_DEPRECATED = 2,
|
||||
STA_OBSOLETE = 3, STA_INTERNAL = 4;
|
||||
public static final int VIS = 1, VIS_PACKAGE = 0, VIS_PUBLIC= 1, VIS_PROTECTED = 2,
|
||||
VIS_PRIVATE = 3;
|
||||
public static final int STK = 2, STK_STATIC = 1;
|
||||
public static final int FIN = 3, FIN_FINAL = 1;
|
||||
public static final int SYN = 4, SYN_SYNCHRONIZED = 1;
|
||||
public static final int ABS = 5, ABS_ABSTRACT = 1;
|
||||
public static final int CAT = 6, CAT_CLASS = 0, CAT_FIELD = 1, CAT_CONSTRUCTOR = 2, CAT_METHOD = 3;
|
||||
public static final int CAT = 6, CAT_CLASS = 0, CAT_FIELD = 1, CAT_CONSTRUCTOR = 2,
|
||||
CAT_METHOD = 3;
|
||||
public static final int PAK = 7;
|
||||
public static final int CLS = 8;
|
||||
public static final int NAM = 9;
|
||||
@ -45,11 +48,14 @@ class APIInfo {
|
||||
public static final char SEP = ';';
|
||||
|
||||
// Internal State
|
||||
private int info; // information about numeric values packed into an int as variable-length nibbles
|
||||
private String pack = ""; // package
|
||||
|
||||
private int info; // information about numeric values packed into an int
|
||||
// as variable-length nibbles
|
||||
private String pack = ""; // package
|
||||
private String cls = ""; // enclosing class
|
||||
private String name = ""; // name
|
||||
private String sig = ""; // signature, class: inheritance, method: signature, field: type, const: signature
|
||||
private String name = ""; // name
|
||||
private String sig = ""; // signature, class: inheritance, method: signature,
|
||||
// field: type, const: signature
|
||||
private String exc = ""; // throws
|
||||
private String stver = ""; // status version
|
||||
|
||||
@ -208,7 +214,8 @@ class APIInfo {
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("unrecognized value '" + val + "' for type '" + typeNames[typ] + "'");
|
||||
throw new IllegalArgumentException(
|
||||
"unrecognized value '" + val + "' for type '" + typeNames[typ] + "'");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -377,7 +384,8 @@ class APIInfo {
|
||||
boolean rcls = rhi.getVal(CAT) == CAT_CLASS;
|
||||
result = lcls == rcls ? 0 : (lcls ? -1 : 1);
|
||||
if (result == 0) {
|
||||
result = (lcls ? lhi.name : lhi.cls).compareTo(rcls ? rhi.name : rhi.cls);
|
||||
result = (lcls ? lhi.name : lhi.cls).compareTo(
|
||||
rcls ? rhi.name : rhi.cls);
|
||||
if (result == 0) {
|
||||
result = lhi.getVal(CAT)- rhi.getVal(CAT);
|
||||
if (result == 0) {
|
||||
|
@ -17,27 +17,27 @@
|
||||
* - abstract or non-abstract (AB NA)
|
||||
* - constructor, member, field (C M F)
|
||||
*
|
||||
* Requires JDK 1.4.2 or later
|
||||
* Requires JDK 1.5 or later
|
||||
*
|
||||
* Sample compilation:
|
||||
* c:/doug/java/jdk1.4.2/build/windows-i586/bin/javac *.java
|
||||
* c:/doug/java/jdk1.5/build/windows-i586/bin/javac *.java
|
||||
*
|
||||
* Sample execution
|
||||
* c:/j2sdk1.4.2/bin/javadoc
|
||||
* -classpath c:/jd2sk1.4.2/lib/tools.jar
|
||||
* c:/j2sdk1.5/bin/javadoc
|
||||
* -classpath c:/jd2sk1.5/lib/tools.jar
|
||||
* -doclet com.ibm.icu.dev.tool.docs.GatherAPIData
|
||||
* -docletpath c:/doug/cvsproj/icu4j/src
|
||||
* -sourcepath c:/doug/cvsproj/icu4j/src
|
||||
* -name "ICU4J 3.0"
|
||||
* -output icu4j30.api
|
||||
* -docletpath c:/doug/icu4j/tools/build/out/lib/icu4j-build-tools.jar
|
||||
* -sourcepath c:/doug/icu4j/main/classes/core/src
|
||||
* -name "ICU4J 4.2"
|
||||
* -output icu4j42.api2
|
||||
* -gzip
|
||||
* -source 1.4
|
||||
* -source 1.5
|
||||
* com.ibm.icu.lang com.ibm.icu.math com.ibm.icu.text com.ibm.icu.util
|
||||
*
|
||||
* todo: provide command-line control of filters of which subclasses/packages to process
|
||||
* todo: record full inheritance heirarchy, not just immediate inheritance
|
||||
* todo: record full inheritance hierarchy, not just immediate inheritance
|
||||
* todo: allow for aliasing comparisons (force (pkg.)*class to be treated as though it
|
||||
* were in a different pkg/class heirarchy (facilitates comparison of icu4j and java)
|
||||
* were in a different pkg/class hierarchy (facilitates comparison of icu4j and java)
|
||||
*/
|
||||
|
||||
package com.ibm.icu.dev.tool.docs;
|
||||
@ -58,9 +58,10 @@ import java.util.zip.ZipOutputStream;
|
||||
|
||||
import com.sun.javadoc.ClassDoc;
|
||||
import com.sun.javadoc.ConstructorDoc;
|
||||
import com.sun.javadoc.Doc;
|
||||
import com.sun.javadoc.ExecutableMemberDoc;
|
||||
import com.sun.javadoc.FieldDoc;
|
||||
import com.sun.javadoc.LanguageVersion;
|
||||
import com.sun.javadoc.MemberDoc;
|
||||
import com.sun.javadoc.MethodDoc;
|
||||
import com.sun.javadoc.ProgramElementDoc;
|
||||
import com.sun.javadoc.RootDoc;
|
||||
@ -103,6 +104,14 @@ public class GatherAPIData {
|
||||
return new GatherAPIData(root).run();
|
||||
}
|
||||
|
||||
/**
|
||||
* If you don't do this, javadoc treats enums like regular classes!
|
||||
* doesn't matter if you pass -source 1.5 or not.
|
||||
*/
|
||||
public static LanguageVersion languageVersion() {
|
||||
return LanguageVersion.JAVA_1_5;
|
||||
}
|
||||
|
||||
GatherAPIData(RootDoc root) {
|
||||
this.root = root;
|
||||
|
||||
@ -192,7 +201,9 @@ public class GatherAPIData {
|
||||
doDocs(cdoc.fields());
|
||||
doDocs(cdoc.constructors());
|
||||
doDocs(cdoc.methods());
|
||||
doDocs(cdoc.innerClasses());
|
||||
// don't call this to iterate over inner classes,
|
||||
// root.classes already includes them
|
||||
// doDocs(cdoc.innerClasses());
|
||||
}
|
||||
|
||||
APIInfo info = createInfo(doc);
|
||||
@ -201,13 +212,70 @@ public class GatherAPIData {
|
||||
}
|
||||
}
|
||||
|
||||
// Sigh. Javadoc doesn't indicate when the compiler generates
|
||||
// the values and valueOf enum methods. The position of the
|
||||
// method for these is not always the same as the position of
|
||||
// the class, though it often is, so we can't use that.
|
||||
|
||||
private boolean isIgnoredEnumMethod(ProgramElementDoc doc) {
|
||||
if (doc.isMethod() && doc.containingClass().isEnum()) {
|
||||
// System.out.println("*** " + doc.qualifiedName() + " pos: " +
|
||||
// doc.position().line() +
|
||||
// " containined by: " +
|
||||
// doc.containingClass().name() +
|
||||
// " pos: " +
|
||||
// doc.containingClass().position().line());
|
||||
// return doc.position().line() == doc.containingClass().position().line();
|
||||
|
||||
String name = doc.name();
|
||||
// assume we don't have enums that overload these method names.
|
||||
return "values".equals(name) || "valueOf".equals(name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// isSynthesized also doesn't seem to work. Let's do this, documenting
|
||||
// synthesized constructors for abstract classes is kind of weird.
|
||||
// We can't actually tell if the constructor was synthesized or is
|
||||
// actually in the docs, but this shouldn't matter. We don't really
|
||||
// care if we didn't properly document the draft status of
|
||||
// default constructors for abstract classes.
|
||||
|
||||
private boolean isAbstractClassDefaultConstructor(ProgramElementDoc doc) {
|
||||
return doc.isConstructor()
|
||||
&& doc.containingClass().isAbstract()
|
||||
&& "()".equals(((ConstructorDoc) doc).signature());
|
||||
}
|
||||
|
||||
private boolean ignore(ProgramElementDoc doc) {
|
||||
if (doc == null) return true;
|
||||
if (doc.isPrivate() || doc.isPackagePrivate()) return true;
|
||||
if (doc instanceof ConstructorDoc && ((ConstructorDoc)doc).isSynthetic()) return true;
|
||||
if (doc.qualifiedName().indexOf(".misc") != -1) {
|
||||
System.out.println("misc: " + doc.qualifiedName()); return true;
|
||||
if (doc instanceof MemberDoc && ((MemberDoc)doc).isSynthetic()) return true;
|
||||
if (doc.qualifiedName().indexOf(".misc") != -1) {
|
||||
System.out.println("misc: " + doc.qualifiedName()); return true;
|
||||
}
|
||||
if (isIgnoredEnumMethod(doc)) {
|
||||
return true;
|
||||
}
|
||||
if (isAbstractClassDefaultConstructor(doc)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (false && doc.qualifiedName().indexOf("LocaleDisplayNames") != -1) {
|
||||
System.err.print("*** " + doc.qualifiedName() + ":");
|
||||
if (doc.isClass()) System.err.print(" class");
|
||||
if (doc.isConstructor()) System.err.print(" constructor");
|
||||
if (doc.isEnum()) System.err.print(" enum");
|
||||
if (doc.isEnumConstant()) System.err.print(" enum_constant");
|
||||
if (doc.isError()) System.err.print(" error");
|
||||
if (doc.isException()) System.err.print(" exception");
|
||||
if (doc.isField()) System.err.print(" field");
|
||||
if (doc.isInterface()) System.err.print(" interface");
|
||||
if (doc.isMethod()) System.err.print(" method");
|
||||
if (doc.isOrdinaryClass()) System.err.print(" ordinary_class");
|
||||
System.err.println();
|
||||
}
|
||||
|
||||
if (!internal) { // debug
|
||||
Tag[] tags = doc.tags();
|
||||
for (int i = 0; i < tags.length; ++i) {
|
||||
@ -258,7 +326,7 @@ public class GatherAPIData {
|
||||
if (version) {
|
||||
info.includeStatusVersion(true);
|
||||
}
|
||||
|
||||
|
||||
// status
|
||||
String[] version = new String[1];
|
||||
info.setType(APIInfo.STA, tagStatus(doc, version));
|
||||
@ -301,8 +369,8 @@ public class GatherAPIData {
|
||||
}
|
||||
|
||||
info.setPackage(trimBase(doc.containingPackage().name()));
|
||||
info.setClassName((doc.isClass() || doc.isInterface() || (doc.containingClass() == null))
|
||||
? ""
|
||||
info.setClassName((doc.isClass() || doc.isInterface() || (doc.containingClass() == null))
|
||||
? ""
|
||||
: trimBase(doc.containingClass().name()));
|
||||
info.setName(trimBase(doc.name()));
|
||||
|
||||
@ -312,7 +380,7 @@ public class GatherAPIData {
|
||||
} else if (doc instanceof ClassDoc) {
|
||||
ClassDoc cdoc = (ClassDoc)doc;
|
||||
|
||||
if (cdoc.isClass() && cdoc.isAbstract()) {
|
||||
if (cdoc.isClass() && cdoc.isAbstract()) {
|
||||
// interfaces are abstract by default, don't mark them as abstract
|
||||
info.setAbstract();
|
||||
}
|
||||
@ -358,17 +426,19 @@ public class GatherAPIData {
|
||||
return info;
|
||||
}
|
||||
|
||||
private int tagStatus(final Doc doc, String[] version) {
|
||||
private int tagStatus(final ProgramElementDoc doc, String[] version) {
|
||||
class Result {
|
||||
int res = -1;
|
||||
void set(int val) {
|
||||
void set(int val) {
|
||||
if (res != -1) {
|
||||
if (val == APIInfo.STA_DEPRECATED) {
|
||||
// ok to have both a 'standard' tag and deprecated
|
||||
return;
|
||||
} else if (res != APIInfo.STA_DEPRECATED) {
|
||||
// if already not deprecated, this is an error
|
||||
System.err.println("bad doc: " + doc + " both: " + APIInfo.getTypeValName(APIInfo.STA, res) + " and: " + APIInfo.getTypeValName(APIInfo.STA, val));
|
||||
System.err.println("bad doc: " + doc + " both: "
|
||||
+ APIInfo.getTypeValName(APIInfo.STA, res) + " and: "
|
||||
+ APIInfo.getTypeValName(APIInfo.STA, val));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -483,8 +553,8 @@ public class GatherAPIData {
|
||||
|
||||
private static int tagKindIndex(String kind) {
|
||||
final String[] tagKinds = {
|
||||
"@internal", "@draft", "@stable", "@since", "@deprecated", "@author", "@see", "@version",
|
||||
"@param", "@return", "@throws", "@obsolete", "@exception", "@serial"
|
||||
"@internal", "@draft", "@stable", "@since", "@deprecated", "@author", "@see",
|
||||
"@version", "@param", "@return", "@throws", "@obsolete", "@exception", "@serial"
|
||||
};
|
||||
|
||||
for (int i = 0; i < tagKinds.length; ++i) {
|
||||
|
@ -0,0 +1,497 @@
|
||||
/**
|
||||
*******************************************************************************
|
||||
* Copyright (C) 2004-2010, International Business Machines Corporation and *
|
||||
* others. All Rights Reserved. *
|
||||
*******************************************************************************
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generate a list of ICU's public APIs, sorted by qualified name and signature
|
||||
* public APIs are all non-internal, non-package apis in com.ibm.icu.[lang|math|text|util].
|
||||
* For each API, list
|
||||
* - public, package, protected, or private (PB PK PT PR)
|
||||
* - static or non-static (STK NST)
|
||||
* - final or non-final (FN NF)
|
||||
* - synchronized or non-synchronized (SYN NSY)
|
||||
* - stable, draft, deprecated, obsolete (ST DR DP OB)
|
||||
* - abstract or non-abstract (AB NA)
|
||||
* - constructor, member, field (C M F)
|
||||
*
|
||||
* Requires JDK 1.4.2 or later
|
||||
*
|
||||
* Sample compilation:
|
||||
* c:/doug/java/jdk1.4.2/build/windows-i586/bin/javac *.java
|
||||
*
|
||||
* Sample execution
|
||||
* c:/j2sdk1.4.2/bin/javadoc
|
||||
* -classpath c:/jd2sk1.4.2/lib/tools.jar
|
||||
* -doclet com.ibm.icu.dev.tool.docs.GatherAPIData
|
||||
* -docletpath c:/doug/cvsproj/icu4j/src
|
||||
* -sourcepath c:/doug/cvsproj/icu4j/src
|
||||
* -name "ICU4J 3.0"
|
||||
* -output icu4j30.api
|
||||
* -gzip
|
||||
* -source 1.4
|
||||
* com.ibm.icu.lang com.ibm.icu.math com.ibm.icu.text com.ibm.icu.util
|
||||
*
|
||||
* todo: provide command-line control of filters of which subclasses/packages to process
|
||||
* todo: record full inheritance heirarchy, not just immediate inheritance
|
||||
* todo: allow for aliasing comparisons (force (pkg.)*class to be treated as though it
|
||||
* were in a different pkg/class heirarchy (facilitates comparison of icu4j and java)
|
||||
*/
|
||||
|
||||
package com.ibm.icu.dev.tool.docs;
|
||||
|
||||
// standard release sdk won't work, need internal build to get access to javadoc
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.TreeSet;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import com.sun.javadoc.ClassDoc;
|
||||
import com.sun.javadoc.ConstructorDoc;
|
||||
import com.sun.javadoc.Doc;
|
||||
import com.sun.javadoc.ExecutableMemberDoc;
|
||||
import com.sun.javadoc.FieldDoc;
|
||||
import com.sun.javadoc.MethodDoc;
|
||||
import com.sun.javadoc.ProgramElementDoc;
|
||||
import com.sun.javadoc.RootDoc;
|
||||
import com.sun.javadoc.Tag;
|
||||
|
||||
public class GatherAPIDataOld {
|
||||
RootDoc root;
|
||||
TreeSet results;
|
||||
String srcName = "Current"; // default source name
|
||||
String output; // name of output file to write
|
||||
String base; // strip this prefix
|
||||
Pattern pat;
|
||||
boolean zip;
|
||||
boolean gzip;
|
||||
boolean internal;
|
||||
boolean version;
|
||||
|
||||
public static int optionLength(String option) {
|
||||
if (option.equals("-name")) {
|
||||
return 2;
|
||||
} else if (option.equals("-output")) {
|
||||
return 2;
|
||||
} else if (option.equals("-base")) {
|
||||
return 2;
|
||||
} else if (option.equals("-filter")) {
|
||||
return 2;
|
||||
} else if (option.equals("-zip")) {
|
||||
return 1;
|
||||
} else if (option.equals("-gzip")) {
|
||||
return 1;
|
||||
} else if (option.equals("-internal")) {
|
||||
return 1;
|
||||
} else if (option.equals("-version")) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static boolean start(RootDoc root) {
|
||||
return new GatherAPIDataOld(root).run();
|
||||
}
|
||||
|
||||
GatherAPIDataOld(RootDoc root) {
|
||||
this.root = root;
|
||||
|
||||
String[][] options = root.options();
|
||||
for (int i = 0; i < options.length; ++i) {
|
||||
String opt = options[i][0];
|
||||
if (opt.equals("-name")) {
|
||||
this.srcName = options[i][1];
|
||||
} else if (opt.equals("-output")) {
|
||||
this.output = options[i][1];
|
||||
} else if (opt.equals("-base")) {
|
||||
this.base = options[i][1]; // should not include '.'
|
||||
} else if (opt.equals("-filter")) {
|
||||
this.pat = Pattern.compile(options[i][1], Pattern.CASE_INSENSITIVE);
|
||||
} else if (opt.equals("-zip")) {
|
||||
this.zip = true;
|
||||
} else if (opt.equals("-gzip")) {
|
||||
this.gzip = true;
|
||||
} else if (opt.equals("-internal")) {
|
||||
this.internal = true;
|
||||
} else if (opt.equals("-version")) {
|
||||
this.version = true;
|
||||
}
|
||||
}
|
||||
|
||||
results = new TreeSet(APIInfo.defaultComparator());
|
||||
}
|
||||
|
||||
private boolean run() {
|
||||
doDocs(root.classes());
|
||||
|
||||
OutputStream os = System.out;
|
||||
if (output != null) {
|
||||
try {
|
||||
if (zip) {
|
||||
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(output + ".zip"));
|
||||
zos.putNextEntry(new ZipEntry(output));
|
||||
os = zos;
|
||||
} else if (gzip) {
|
||||
os = new GZIPOutputStream(new FileOutputStream(output + ".gz"));
|
||||
} else {
|
||||
os = new FileOutputStream(output);
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
RuntimeException re = new RuntimeException(e.getMessage());
|
||||
re.initCause(e);
|
||||
throw re;
|
||||
}
|
||||
}
|
||||
|
||||
BufferedWriter bw = null;
|
||||
try {
|
||||
OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8");
|
||||
bw = new BufferedWriter(osw);
|
||||
|
||||
// writing data file
|
||||
bw.write(String.valueOf(APIInfo.VERSION) + APIInfo.SEP); // header version
|
||||
bw.write(srcName + APIInfo.SEP); // source name
|
||||
bw.write((base == null ? "" : base) + APIInfo.SEP); // base
|
||||
bw.newLine();
|
||||
writeResults(results, bw);
|
||||
bw.close(); // should flush, close all, etc
|
||||
} catch (IOException e) {
|
||||
try { bw.close(); } catch (IOException e2) {}
|
||||
RuntimeException re = new RuntimeException("write error: " + e.getMessage());
|
||||
re.initCause(e);
|
||||
throw re;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void doDocs(ProgramElementDoc[] docs) {
|
||||
if (docs != null && docs.length > 0) {
|
||||
for (int i = 0; i < docs.length; ++i) {
|
||||
doDoc(docs[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void doDoc(ProgramElementDoc doc) {
|
||||
if (ignore(doc)) return;
|
||||
|
||||
if (doc.isClass() || doc.isInterface()) {
|
||||
ClassDoc cdoc = (ClassDoc)doc;
|
||||
doDocs(cdoc.fields());
|
||||
doDocs(cdoc.constructors());
|
||||
doDocs(cdoc.methods());
|
||||
doDocs(cdoc.innerClasses());
|
||||
}
|
||||
|
||||
APIInfo info = createInfo(doc);
|
||||
if (info != null) {
|
||||
results.add(info);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean ignore(ProgramElementDoc doc) {
|
||||
if (doc == null) return true;
|
||||
if (doc.isPrivate() || doc.isPackagePrivate()) return true;
|
||||
if (doc instanceof ConstructorDoc && ((ConstructorDoc)doc).isSynthetic()) return true;
|
||||
if (doc.qualifiedName().indexOf(".misc") != -1) {
|
||||
System.out.println("misc: " + doc.qualifiedName()); return true;
|
||||
}
|
||||
if (!internal) { // debug
|
||||
Tag[] tags = doc.tags();
|
||||
for (int i = 0; i < tags.length; ++i) {
|
||||
if (tagKindIndex(tags[i].kind()) == INTERNAL) { return true; }
|
||||
}
|
||||
}
|
||||
if (pat != null && (doc.isClass() || doc.isInterface())) {
|
||||
if (!pat.matcher(doc.name()).matches()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void writeResults(Collection c, BufferedWriter w) {
|
||||
Iterator iter = c.iterator();
|
||||
while (iter.hasNext()) {
|
||||
APIInfo info = (APIInfo)iter.next();
|
||||
info.writeln(w);
|
||||
}
|
||||
}
|
||||
|
||||
private String trimBase(String arg) {
|
||||
if (base != null) {
|
||||
for (int n = arg.indexOf(base); n != -1; n = arg.indexOf(base, n)) {
|
||||
arg = arg.substring(0, n) + arg.substring(n+base.length());
|
||||
}
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
|
||||
public APIInfo createInfo(ProgramElementDoc doc) {
|
||||
|
||||
// Doc. name
|
||||
// Doc. isField, isMethod, isConstructor, isClass, isInterface
|
||||
// ProgramElementDoc. containingClass, containingPackage
|
||||
// ProgramElementDoc. isPublic, isProtected, isPrivate, isPackagePrivate
|
||||
// ProgramElementDoc. isStatic, isFinal
|
||||
// MemberDoc.isSynthetic
|
||||
// ExecutableMemberDoc isSynchronized, signature
|
||||
// Type.toString() // e.g. "String[][]"
|
||||
// ClassDoc.isAbstract, superClass, interfaces, fields, methods, constructors, innerClasses
|
||||
// FieldDoc type
|
||||
// ConstructorDoc qualifiedName
|
||||
// MethodDoc isAbstract, returnType
|
||||
|
||||
APIInfo info = new APIInfo();
|
||||
if (version) {
|
||||
info.includeStatusVersion(true);
|
||||
}
|
||||
|
||||
// status
|
||||
String[] version = new String[1];
|
||||
info.setType(APIInfo.STA, tagStatus(doc, version));
|
||||
info.setStatusVersion(version[0]);
|
||||
|
||||
// visibility
|
||||
if (doc.isPublic()) {
|
||||
info.setPublic();
|
||||
} else if (doc.isProtected()) {
|
||||
info.setProtected();
|
||||
} else if (doc.isPrivate()) {
|
||||
info.setPrivate();
|
||||
} else {
|
||||
// default is package
|
||||
}
|
||||
|
||||
// static
|
||||
if (doc.isStatic()) {
|
||||
info.setStatic();
|
||||
} else {
|
||||
// default is non-static
|
||||
}
|
||||
|
||||
// final
|
||||
if (doc.isFinal()) {
|
||||
info.setFinal();
|
||||
} else {
|
||||
// default is non-final
|
||||
}
|
||||
|
||||
// type
|
||||
if (doc.isField()) {
|
||||
info.setField();
|
||||
} else if (doc.isMethod()) {
|
||||
info.setMethod();
|
||||
} else if (doc.isConstructor()) {
|
||||
info.setConstructor();
|
||||
} else if (doc.isClass() || doc.isInterface()) {
|
||||
info.setClass();
|
||||
}
|
||||
|
||||
info.setPackage(trimBase(doc.containingPackage().name()));
|
||||
info.setClassName((doc.isClass() || doc.isInterface() || (doc.containingClass() == null))
|
||||
? ""
|
||||
: trimBase(doc.containingClass().name()));
|
||||
info.setName(trimBase(doc.name()));
|
||||
|
||||
if (doc instanceof FieldDoc) {
|
||||
FieldDoc fdoc = (FieldDoc)doc;
|
||||
info.setSignature(trimBase(fdoc.type().toString()));
|
||||
} else if (doc instanceof ClassDoc) {
|
||||
ClassDoc cdoc = (ClassDoc)doc;
|
||||
|
||||
if (cdoc.isClass() && cdoc.isAbstract()) {
|
||||
// interfaces are abstract by default, don't mark them as abstract
|
||||
info.setAbstract();
|
||||
}
|
||||
|
||||
StringBuffer buf = new StringBuffer();
|
||||
if (cdoc.isClass()) {
|
||||
buf.append("extends ");
|
||||
buf.append(cdoc.superclass().qualifiedName());
|
||||
}
|
||||
ClassDoc[] imp = cdoc.interfaces();
|
||||
if (imp != null && imp.length > 0) {
|
||||
if (buf.length() > 0) {
|
||||
buf.append(" ");
|
||||
}
|
||||
buf.append("implements");
|
||||
for (int i = 0; i < imp.length; ++i) {
|
||||
if (i != 0) {
|
||||
buf.append(",");
|
||||
}
|
||||
buf.append(" ");
|
||||
buf.append(imp[i].qualifiedName());
|
||||
}
|
||||
}
|
||||
info.setSignature(trimBase(buf.toString()));
|
||||
} else {
|
||||
ExecutableMemberDoc emdoc = (ExecutableMemberDoc)doc;
|
||||
if (emdoc.isSynchronized()) {
|
||||
info.setSynchronized();
|
||||
}
|
||||
|
||||
if (doc instanceof MethodDoc) {
|
||||
MethodDoc mdoc = (MethodDoc)doc;
|
||||
if (mdoc.isAbstract()) {
|
||||
info.setAbstract();
|
||||
}
|
||||
info.setSignature(trimBase(mdoc.returnType().toString() + emdoc.signature()));
|
||||
} else {
|
||||
// constructor
|
||||
info.setSignature(trimBase(emdoc.signature()));
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private int tagStatus(final Doc doc, String[] version) {
|
||||
class Result {
|
||||
int res = -1;
|
||||
void set(int val) {
|
||||
if (res != -1) {
|
||||
if (val == APIInfo.STA_DEPRECATED) {
|
||||
// ok to have both a 'standard' tag and deprecated
|
||||
return;
|
||||
} else if (res != APIInfo.STA_DEPRECATED) {
|
||||
// if already not deprecated, this is an error
|
||||
System.err.println("bad doc: " + doc + " both: " + APIInfo.getTypeValName(APIInfo.STA, res) + " and: " + APIInfo.getTypeValName(APIInfo.STA, val));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// ok to replace with new tag
|
||||
res = val;
|
||||
}
|
||||
int get() {
|
||||
if (res == -1) {
|
||||
System.err.println("warning: no tag for " + doc);
|
||||
return 0;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
Tag[] tags = doc.tags();
|
||||
Result result = new Result();
|
||||
String statusVer = "";
|
||||
for (int i = 0; i < tags.length; ++i) {
|
||||
Tag tag = tags[i];
|
||||
|
||||
String kind = tag.kind();
|
||||
int ix = tagKindIndex(kind);
|
||||
|
||||
switch (ix) {
|
||||
case INTERNAL:
|
||||
result.set(internal ? APIInfo.STA_INTERNAL : -2); // -2 for legacy compatibility
|
||||
statusVer = getStatusVersion(tag);
|
||||
break;
|
||||
|
||||
case DRAFT:
|
||||
result.set(APIInfo.STA_DRAFT);
|
||||
statusVer = getStatusVersion(tag);
|
||||
break;
|
||||
|
||||
case STABLE:
|
||||
result.set(APIInfo.STA_STABLE);
|
||||
statusVer = getStatusVersion(tag);
|
||||
break;
|
||||
|
||||
case DEPRECATED:
|
||||
result.set(APIInfo.STA_DEPRECATED);
|
||||
statusVer = getStatusVersion(tag);
|
||||
break;
|
||||
|
||||
case OBSOLETE:
|
||||
result.set(APIInfo.STA_OBSOLETE);
|
||||
statusVer = getStatusVersion(tag);
|
||||
break;
|
||||
|
||||
case SINCE:
|
||||
case EXCEPTION:
|
||||
case VERSION:
|
||||
case UNKNOWN:
|
||||
case AUTHOR:
|
||||
case SEE:
|
||||
case PARAM:
|
||||
case RETURN:
|
||||
case THROWS:
|
||||
case SERIAL:
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException("unknown index " + ix + " for tag: " + kind);
|
||||
}
|
||||
}
|
||||
|
||||
if (version != null) {
|
||||
version[0] = statusVer;
|
||||
}
|
||||
return result.get();
|
||||
}
|
||||
|
||||
private String getStatusVersion(Tag tag) {
|
||||
String text = tag.text();
|
||||
if (text != null && text.length() > 0) {
|
||||
// Extract version string
|
||||
int start = -1;
|
||||
int i = 0;
|
||||
for (; i < text.length(); i++) {
|
||||
char ch = text.charAt(i);
|
||||
if (ch == '.' || (ch >= '0' && ch <= '9')) {
|
||||
if (start == -1) {
|
||||
start = i;
|
||||
}
|
||||
} else if (start != -1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (start != -1) {
|
||||
return text.substring(start, i);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static final int UNKNOWN = -1;
|
||||
private static final int INTERNAL = 0;
|
||||
private static final int DRAFT = 1;
|
||||
private static final int STABLE = 2;
|
||||
private static final int SINCE = 3;
|
||||
private static final int DEPRECATED = 4;
|
||||
private static final int AUTHOR = 5;
|
||||
private static final int SEE = 6;
|
||||
private static final int VERSION = 7;
|
||||
private static final int PARAM = 8;
|
||||
private static final int RETURN = 9;
|
||||
private static final int THROWS = 10;
|
||||
private static final int OBSOLETE = 11;
|
||||
private static final int EXCEPTION = 12;
|
||||
private static final int SERIAL = 13;
|
||||
|
||||
private static int tagKindIndex(String kind) {
|
||||
final String[] tagKinds = {
|
||||
"@internal", "@draft", "@stable", "@since", "@deprecated", "@author", "@see", "@version",
|
||||
"@param", "@return", "@throws", "@obsolete", "@exception", "@serial"
|
||||
};
|
||||
|
||||
for (int i = 0; i < tagKinds.length; ++i) {
|
||||
if (kind.equals(tagKinds[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
@ -46,11 +46,11 @@ public class ReportAPI {
|
||||
boolean html;
|
||||
String outputFile;
|
||||
|
||||
TreeSet added;
|
||||
TreeSet removed;
|
||||
TreeSet promoted;
|
||||
TreeSet obsoleted;
|
||||
ArrayList changed;
|
||||
TreeSet<APIInfo> added;
|
||||
TreeSet<APIInfo> removed;
|
||||
TreeSet<APIInfo> promoted;
|
||||
TreeSet<APIInfo> obsoleted;
|
||||
ArrayList<DeltaInfo> changed;
|
||||
|
||||
static final class DeltaInfo extends APIInfo {
|
||||
APIInfo added;
|
||||
@ -132,25 +132,25 @@ public class ReportAPI {
|
||||
this.oldData = oldData;
|
||||
this.newData = newData;
|
||||
|
||||
removed = (TreeSet)oldData.set.clone();
|
||||
removed = (TreeSet<APIInfo>)oldData.set.clone();
|
||||
removed.removeAll(newData.set);
|
||||
|
||||
added = (TreeSet)newData.set.clone();
|
||||
added = (TreeSet<APIInfo>)newData.set.clone();
|
||||
added.removeAll(oldData.set);
|
||||
|
||||
changed = new ArrayList();
|
||||
Iterator ai = added.iterator();
|
||||
Iterator ri = removed.iterator();
|
||||
Comparator c = APIInfo.changedComparator();
|
||||
changed = new ArrayList<DeltaInfo>();
|
||||
Iterator<APIInfo> ai = added.iterator();
|
||||
Iterator<APIInfo> ri = removed.iterator();
|
||||
Comparator<APIInfo> c = APIInfo.changedComparator();
|
||||
|
||||
ArrayList ams = new ArrayList();
|
||||
ArrayList rms = new ArrayList();
|
||||
ArrayList<APIInfo> ams = new ArrayList<APIInfo>();
|
||||
ArrayList<APIInfo> rms = new ArrayList<APIInfo>();
|
||||
//PrintWriter outpw = new PrintWriter(System.out);
|
||||
|
||||
APIInfo a = null, r = null;
|
||||
while ((a != null || ai.hasNext()) && (r != null || ri.hasNext())) {
|
||||
if (a == null) a = (APIInfo)ai.next();
|
||||
if (r == null) r = (APIInfo)ri.next();
|
||||
if (a == null) a = ai.next();
|
||||
if (r == null) r = ri.next();
|
||||
|
||||
String am = a.getClassName() + "." + a.getName();
|
||||
String rm = r.getClassName() + "." + r.getName();
|
||||
@ -164,7 +164,7 @@ public class ReportAPI {
|
||||
if (!ams.isEmpty()) {
|
||||
// simplest case first
|
||||
if (ams.size() == 1 && rms.size() == 1) {
|
||||
changed.add(new DeltaInfo((APIInfo)ams.get(0), (APIInfo)rms.get(0)));
|
||||
changed.add(new DeltaInfo(ams.get(0), rms.get(0)));
|
||||
} else {
|
||||
// dang, what to do now?
|
||||
// TODO: modify deltainfo to deal with lists of added and removed
|
||||
@ -186,33 +186,33 @@ public class ReportAPI {
|
||||
}
|
||||
|
||||
// now clean up added and removed by cleaning out the changed members
|
||||
Iterator ci = changed.iterator();
|
||||
Iterator<DeltaInfo> ci = changed.iterator();
|
||||
while (ci.hasNext()) {
|
||||
DeltaInfo di = (DeltaInfo)ci.next();
|
||||
DeltaInfo di = ci.next();
|
||||
added.remove(di.added);
|
||||
removed.remove(di.removed);
|
||||
}
|
||||
|
||||
Set tempAdded = new HashSet();
|
||||
Set<APIInfo> tempAdded = new HashSet<APIInfo>();
|
||||
tempAdded.addAll(newData.set);
|
||||
tempAdded.removeAll(removed);
|
||||
TreeSet changedAdded = new TreeSet(APIInfo.defaultComparator());
|
||||
TreeSet<APIInfo> changedAdded = new TreeSet<APIInfo>(APIInfo.defaultComparator());
|
||||
changedAdded.addAll(tempAdded);
|
||||
|
||||
Set tempRemoved = new HashSet();
|
||||
Set<APIInfo> tempRemoved = new HashSet<APIInfo>();
|
||||
tempRemoved.addAll(oldData.set);
|
||||
tempRemoved.removeAll(added);
|
||||
TreeSet changedRemoved = new TreeSet(APIInfo.defaultComparator());
|
||||
TreeSet<APIInfo> changedRemoved = new TreeSet<APIInfo>(APIInfo.defaultComparator());
|
||||
changedRemoved.addAll(tempRemoved);
|
||||
|
||||
promoted = new TreeSet(APIInfo.defaultComparator());
|
||||
obsoleted = new TreeSet(APIInfo.defaultComparator());
|
||||
promoted = new TreeSet<APIInfo>(APIInfo.defaultComparator());
|
||||
obsoleted = new TreeSet<APIInfo>(APIInfo.defaultComparator());
|
||||
ai = changedAdded.iterator();
|
||||
ri = changedRemoved.iterator();
|
||||
a = r = null;
|
||||
while ((a != null || ai.hasNext()) && (r != null || ri.hasNext())) {
|
||||
if (a == null) a = (APIInfo)ai.next();
|
||||
if (r == null) r = (APIInfo)ri.next();
|
||||
if (a == null) a = ai.next();
|
||||
if (r == null) r = ri.next();
|
||||
int result = c.compare(a, r);
|
||||
if (result < 0) {
|
||||
a = null;
|
||||
@ -243,7 +243,9 @@ public class ReportAPI {
|
||||
}
|
||||
}
|
||||
int lstatus = lhs.getVal(APIInfo.STA);
|
||||
if (lstatus == APIInfo.STA_OBSOLETE || lstatus == APIInfo.STA_DEPRECATED || lstatus == APIInfo.STA_INTERNAL) {
|
||||
if (lstatus == APIInfo.STA_OBSOLETE
|
||||
|| lstatus == APIInfo.STA_DEPRECATED
|
||||
|| lstatus == APIInfo.STA_INTERNAL) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
@ -274,7 +276,8 @@ public class ReportAPI {
|
||||
String year = fmt.format(new Date());
|
||||
String title = "ICU4J API Comparison: " + oldData.name + " with " + newData.name;
|
||||
String info = "Contents generated by ReportAPI tool on " + new Date().toString();
|
||||
String copyright = "Copyright (C) " + year + ", International Business Machines Corporation, All Rights Reserved.";
|
||||
String copyright = "Copyright (C) " + year +
|
||||
", International Business Machines Corporation, All Rights Reserved.";
|
||||
|
||||
if (html) {
|
||||
pw.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">");
|
||||
@ -403,12 +406,13 @@ public class ReportAPI {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void printResults(Collection c, PrintWriter pw, boolean html, boolean isChangedAPIs) {
|
||||
Iterator iter = c.iterator();
|
||||
private static void printResults(Collection<? extends APIInfo> c, PrintWriter pw, boolean html,
|
||||
boolean isChangedAPIs) {
|
||||
Iterator<? extends APIInfo> iter = c.iterator();
|
||||
String pack = null;
|
||||
String clas = null;
|
||||
while (iter.hasNext()) {
|
||||
APIInfo info = (APIInfo)iter.next();
|
||||
APIInfo info = iter.next();
|
||||
|
||||
String packageName = info.getPackageName();
|
||||
if (!packageName.equals(pack)) {
|
||||
@ -471,19 +475,19 @@ public class ReportAPI {
|
||||
pw.println();
|
||||
}
|
||||
|
||||
private static TreeSet stripAndResort(TreeSet t) {
|
||||
private static TreeSet<APIInfo> stripAndResort(TreeSet<APIInfo> t) {
|
||||
stripClassInfo(t);
|
||||
TreeSet r = new TreeSet(APIInfo.classFirstComparator());
|
||||
TreeSet<APIInfo> r = new TreeSet<APIInfo>(APIInfo.classFirstComparator());
|
||||
r.addAll(t);
|
||||
return r;
|
||||
}
|
||||
|
||||
private static void stripClassInfo(Collection c) {
|
||||
private static void stripClassInfo(Collection<APIInfo> c) {
|
||||
// c is sorted with class info first
|
||||
Iterator iter = c.iterator();
|
||||
Iterator<? extends APIInfo> iter = c.iterator();
|
||||
String cname = null;
|
||||
while (iter.hasNext()) {
|
||||
APIInfo info = (APIInfo)iter.next();
|
||||
APIInfo info = iter.next();
|
||||
String className = info.getClassName();
|
||||
if (cname != null) {
|
||||
if (cname.equals(className)) {
|
||||
|
Loading…
Reference in New Issue
Block a user