| Method from org.apache.catalina.realm.JNDIRealm Detail: |
public Principal authenticate(String username,
String credentials) {
DirContext context = null;
Principal principal = null;
try {
// Ensure that we have a directory context available
context = open();
// Occassionally the directory context will timeout. Try one more
// time before giving up.
try {
// Authenticate the specified username if possible
principal = authenticate(context, username, credentials);
} catch (NullPointerException e) {
/* BZ 42449 - Kludge Sun's LDAP provider
with broken SSL
*/
// log the exception so we know it's there.
containerLog.warn(sm.getString("jndiRealm.exception"), e);
// close the connection so we know it will be reopened.
if (context != null)
close(context);
// open a new directory context.
context = open();
// Try the authentication again.
principal = authenticate(context, username, credentials);
} catch (CommunicationException e) {
// log the exception so we know it's there.
containerLog.warn(sm.getString("jndiRealm.exception"), e);
// close the connection so we know it will be reopened.
if (context != null)
close(context);
// open a new directory context.
context = open();
// Try the authentication again.
principal = authenticate(context, username, credentials);
} catch (ServiceUnavailableException e) {
// log the exception so we know it's there.
containerLog.warn(sm.getString("jndiRealm.exception"), e);
// close the connection so we know it will be reopened.
if (context != null)
close(context);
// open a new directory context.
context = open();
// Try the authentication again.
principal = authenticate(context, username, credentials);
}
// Release this context
release(context);
// Return the authenticated Principal (if any)
return (principal);
} catch (NamingException e) {
// Log the problem for posterity
containerLog.error(sm.getString("jndiRealm.exception"), e);
// Close the connection so that it gets reopened next time
if (context != null)
close(context);
// Return "not authenticated" for this request
return (null);
}
}
Return the Principal associated with the specified username and
credentials, if there is one; otherwise return null.
If there are any errors with the JDBC connection, executing
the query or anything we return null (don't authenticate). This
event is also logged, and the connection will be closed so that
a subsequent request will automatically re-open it. |
public synchronized Principal authenticate(DirContext context,
String username,
String credentials) throws NamingException {
if (username == null || username.equals("")
|| credentials == null || credentials.equals(""))
return (null);
if (userPatternArray != null) {
for (curUserPattern = 0;
curUserPattern < userPatternFormatArray.length;
curUserPattern++) {
// Retrieve user information
User user = getUser(context, username);
if (user != null) {
try {
// Check the user's credentials
if (checkCredentials(context, user, credentials)) {
// Search for additional roles
List< String > roles = getRoles(context, user);
return (new GenericPrincipal(this,
username,
credentials,
roles));
}
} catch (InvalidNameException ine) {
// Log the problem for posterity
containerLog.warn(sm.getString("jndiRealm.exception"), ine);
// ignore; this is probably due to a name not fitting
// the search path format exactly, as in a fully-
// qualified name being munged into a search path
// that already contains cn= or vice-versa
}
}
}
return null;
} else {
// Retrieve user information
User user = getUser(context, username);
if (user == null)
return (null);
// Check the user's credentials
if (!checkCredentials(context, user, credentials))
return (null);
// Search for additional roles
List< String > roles = getRoles(context, user);
// Create and return a suitable Principal for this user
return (new GenericPrincipal(this, username, credentials, roles));
}
}
Return the Principal associated with the specified username and
credentials, if there is one; otherwise return null. |
protected boolean bindAsUser(DirContext context,
User user,
String credentials) throws NamingException {
if (credentials == null || user == null)
return (false);
String dn = user.dn;
if (dn == null)
return (false);
// Validate the credentials specified by the user
if (containerLog.isTraceEnabled()) {
containerLog.trace(" validating credentials by binding as the user");
}
// Set up security environment to bind as the user
context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
context.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials);
// Elicit an LDAP bind operation
boolean validated = false;
try {
if (containerLog.isTraceEnabled()) {
containerLog.trace(" binding as " + dn);
}
context.getAttributes("", null);
validated = true;
}
catch (AuthenticationException e) {
if (containerLog.isTraceEnabled()) {
containerLog.trace(" bind attempt failed");
}
}
// Restore the original security environment
if (connectionName != null) {
context.addToEnvironment(Context.SECURITY_PRINCIPAL,
connectionName);
} else {
context.removeFromEnvironment(Context.SECURITY_PRINCIPAL);
}
if (connectionPassword != null) {
context.addToEnvironment(Context.SECURITY_CREDENTIALS,
connectionPassword);
}
else {
context.removeFromEnvironment(Context.SECURITY_CREDENTIALS);
}
return (validated);
}
Check credentials by binding to the directory as the user |
protected boolean checkCredentials(DirContext context,
User user,
String credentials) throws NamingException {
boolean validated = false;
if (userPassword == null) {
validated = bindAsUser(context, user, credentials);
} else {
validated = compareCredentials(context, user, credentials);
}
if (containerLog.isTraceEnabled()) {
if (validated) {
containerLog.trace(sm.getString("jndiRealm.authenticateSuccess",
user.username));
} else {
containerLog.trace(sm.getString("jndiRealm.authenticateFailure",
user.username));
}
}
return (validated);
}
Check whether the given User can be authenticated with the
given credentials. If the userPassword
configuration attribute is specified, the credentials
previously retrieved from the directory are compared explicitly
with those presented by the user. Otherwise the presented
credentials are checked by binding to the directory as the
user. |
protected void close(DirContext context) {
// Do nothing if there is no opened connection
if (context == null)
return;
// Close our opened connection
try {
if (containerLog.isDebugEnabled())
containerLog.debug("Closing directory context");
context.close();
} catch (NamingException e) {
containerLog.error(sm.getString("jndiRealm.close"), e);
}
this.context = null;
}
Close any open connection to the directory server for this Realm. |
protected boolean compareCredentials(DirContext context,
User info,
String credentials) throws NamingException {
if (info == null || credentials == null)
return (false);
String password = info.password;
if (password == null)
return (false);
// Validate the credentials specified by the user
if (containerLog.isTraceEnabled())
containerLog.trace(" validating credentials");
boolean validated = false;
if (hasMessageDigest()) {
// iPlanet support if the values starts with {SHA1}
// The string is in a format compatible with Base64.encode not
// the Hex encoding of the parent class.
if (password.startsWith("{SHA}")) {
/* sync since super.digest() does this same thing */
synchronized (this) {
password = password.substring(5);
md.reset();
md.update(credentials.getBytes());
String digestedPassword =
new String(Base64.encode(md.digest()));
validated = password.equals(digestedPassword);
}
} else if (password.startsWith("{SSHA}")) {
// Bugzilla 32938
/* sync since super.digest() does this same thing */
synchronized (this) {
password = password.substring(6);
md.reset();
md.update(credentials.getBytes());
// Decode stored password.
ByteChunk pwbc = new ByteChunk(password.length());
try {
pwbc.append(password.getBytes(), 0, password.length());
} catch (IOException e) {
// Should never happen
containerLog.error("Could not append password bytes to chunk: ", e);
}
CharChunk decoded = new CharChunk();
Base64.decode(pwbc, decoded);
char[] pwarray = decoded.getBuffer();
// Split decoded password into hash and salt.
final int saltpos = 20;
byte[] hash = new byte[saltpos];
for (int i=0; i< hash.length; i++) {
hash[i] = (byte) pwarray[i];
}
byte[] salt = new byte[pwarray.length - saltpos];
for (int i=0; i< salt.length; i++)
salt[i] = (byte)pwarray[i+saltpos];
md.update(salt);
byte[] dp = md.digest();
validated = Arrays.equals(dp, hash);
} // End synchronized(this) block
} else {
// Hex hashes should be compared case-insensitive
validated = (digest(credentials).equalsIgnoreCase(password));
}
} else
validated = (digest(credentials).equals(password));
return (validated);
}
Check whether the credentials presented by the user match those
retrieved from the directory. |
protected String doRFC2254Encoding(String inString) {
StringBuffer buf = new StringBuffer(inString.length());
for (int i = 0; i < inString.length(); i++) {
char c = inString.charAt(i);
switch (c) {
case '\\":
buf.append("\\5c");
break;
case '*":
buf.append("\\2a");
break;
case '(":
buf.append("\\28");
break;
case ')":
buf.append("\\29");
break;
case '\0":
buf.append("\\00");
break;
default:
buf.append(c);
break;
}
}
return buf.toString();
}
Given an LDAP search string, returns the string with certain characters
escaped according to RFC 2254 guidelines.
The character mapping is as follows:
char -> Replacement
---------------------------
* -> \2a
( -> \28
) -> \29
\ -> \5c
\0 -> \00 |
public String getAlternateURL() {
return this.alternateURL;
}
Getter for property alternateURL. |
public String getAuthentication() {
// ------------------------------------------------------------- Properties
return authentication;
}
Return the type of authentication to use. |
public String getConnectionName() {
return (this.connectionName);
}
Return the connection username for this Realm. |
public String getConnectionPassword() {
return (this.connectionPassword);
}
Return the connection password for this Realm. |
public String getConnectionURL() {
return (this.connectionURL);
}
Return the connection URL for this Realm. |
public String getContextFactory() {
return (this.contextFactory);
}
Return the JNDI context factory for this Realm. |
public String getDerefAliases() {
return derefAliases;
}
Return the derefAliases setting to be used. |
protected Hashtable getDirectoryContextEnvironment() {
Hashtable< String,String > env = new Hashtable< String,String >();
// Configure our directory context environment.
if (containerLog.isDebugEnabled() && connectionAttempt == 0)
containerLog.debug("Connecting to URL " + connectionURL);
else if (containerLog.isDebugEnabled() && connectionAttempt > 0)
containerLog.debug("Connecting to URL " + alternateURL);
env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory);
if (connectionName != null)
env.put(Context.SECURITY_PRINCIPAL, connectionName);
if (connectionPassword != null)
env.put(Context.SECURITY_CREDENTIALS, connectionPassword);
if (connectionURL != null && connectionAttempt == 0)
env.put(Context.PROVIDER_URL, connectionURL);
else if (alternateURL != null && connectionAttempt > 0)
env.put(Context.PROVIDER_URL, alternateURL);
if (authentication != null)
env.put(Context.SECURITY_AUTHENTICATION, authentication);
if (protocol != null)
env.put(Context.SECURITY_PROTOCOL, protocol);
if (referrals != null)
env.put(Context.REFERRAL, referrals);
if (derefAliases != null)
env.put(JNDIRealm.DEREF_ALIASES, derefAliases);
return env;
}
Create our directory context configuration. |
protected String getName() {
return (name);
}
Return a short name for this Realm implementation. |
protected String getPassword(String username) {
return (null);
}
Return the password associated with the given principal's user name. |
protected Principal getPrincipal(String username) {
DirContext context = null;
Principal principal = null;
try {
// Ensure that we have a directory context available
context = open();
// Occassionally the directory context will timeout. Try one more
// time before giving up.
try {
// Authenticate the specified username if possible
principal = getPrincipal(context, username);
} catch (CommunicationException e) {
// log the exception so we know it's there.
containerLog.warn(sm.getString("jndiRealm.exception"), e);
// close the connection so we know it will be reopened.
if (context != null)
close(context);
// open a new directory context.
context = open();
// Try the authentication again.
principal = getPrincipal(context, username);
} catch (ServiceUnavailableException e) {
// log the exception so we know it's there.
containerLog.warn(sm.getString("jndiRealm.exception"), e);
// close the connection so we know it will be reopened.
if (context != null)
close(context);
// open a new directory context.
context = open();
// Try the authentication again.
principal = getPrincipal(context, username);
}
// Release this context
release(context);
// Return the authenticated Principal (if any)
return (principal);
} catch (NamingException e) {
// Log the problem for posterity
containerLog.error(sm.getString("jndiRealm.exception"), e);
// Close the connection so that it gets reopened next time
if (context != null)
close(context);
// Return "not authenticated" for this request
return (null);
}
}
Return the Principal associated with the given user name. |
protected synchronized Principal getPrincipal(DirContext context,
String username) throws NamingException {
User user = getUser(context, username);
return new GenericPrincipal(this, user.username, user.password ,
getRoles(context, user));
}
Return the Principal associated with the given user name. |
public String getProtocol() {
return protocol;
}
Return the protocol to be used. |
public String getReferrals() {
return referrals;
}
Returns the current settings for handling JNDI referrals. |
public String getRoleBase() {
return (this.roleBase);
}
Return the base element for role searches. |
public String getRoleName() {
return (this.roleName);
}
Return the role name attribute name for this Realm. |
public String getRoleSearch() {
return (this.roleSearch);
}
Return the message format pattern for selecting roles in this Realm. |
public boolean getRoleSubtree() {
return (this.roleSubtree);
}
Return the "search subtree for roles" flag. |
protected List getRoles(DirContext context,
User user) throws NamingException {
if (user == null)
return (null);
String dn = user.dn;
String username = user.username;
if (dn == null || username == null)
return (null);
if (containerLog.isTraceEnabled())
containerLog.trace(" getRoles(" + dn + ")");
// Start with roles retrieved from the user entry
ArrayList< String > list = user.roles;
if (list == null) {
list = new ArrayList< String >();
}
// Are we configured to do role searches?
if ((roleFormat == null) || (roleName == null))
return (list);
// Set up parameters for an appropriate search
String filter = roleFormat.format(new String[] { doRFC2254Encoding(dn), username });
SearchControls controls = new SearchControls();
if (roleSubtree)
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
else
controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
controls.setReturningAttributes(new String[] {roleName});
// Perform the configured search and process the results
NamingEnumeration results =
context.search(roleBase, filter, controls);
if (results == null)
return (list); // Should never happen, but just in case ...
while (results.hasMore()) {
SearchResult result = (SearchResult) results.next();
Attributes attrs = result.getAttributes();
if (attrs == null)
continue;
list = addAttributeValues(roleName, attrs, list);
}
if (containerLog.isTraceEnabled()) {
if (list != null) {
containerLog.trace(" Returning " + list.size() + " roles");
for (int i=0; i< list.size(); i++)
containerLog.trace( " Found role " + list.get(i));
} else {
containerLog.trace(" getRoles about to return null ");
}
}
return (list);
}
Return a List of roles associated with the given User. Any
roles present in the user's directory entry are supplemented by
a directory search. If no roles are associated with this user,
a zero-length List is returned. |
protected User getUser(DirContext context,
String username) throws NamingException {
User user = null;
// Get attributes to retrieve from user entry
ArrayList< String > list = new ArrayList< String >();
if (userPassword != null)
list.add(userPassword);
if (userRoleName != null)
list.add(userRoleName);
String[] attrIds = new String[list.size()];
list.toArray(attrIds);
// Use pattern or search for user entry
if (userPatternFormatArray != null) {
user = getUserByPattern(context, username, attrIds);
} else {
user = getUserBySearch(context, username, attrIds);
}
return user;
}
Return a User object containing information about the user
with the specified username, if found in the directory;
otherwise return null.
If the userPassword configuration attribute is
specified, the value of that attribute is retrieved from the
user's directory entry. If the userRoleName
configuration attribute is specified, all values of that
attribute are retrieved from the directory entry. |
public String getUserBase() {
return (this.userBase);
}
Return the base element for user searches. |
protected User getUserByPattern(DirContext context,
String username,
String[] attrIds) throws NamingException {
if (username == null || userPatternFormatArray[curUserPattern] == null)
return (null);
// Form the dn from the user pattern
String dn = userPatternFormatArray[curUserPattern].format(new String[] { username });
// Get required attributes from user entry
Attributes attrs = null;
try {
attrs = context.getAttributes(dn, attrIds);
} catch (NameNotFoundException e) {
return (null);
}
if (attrs == null)
return (null);
// Retrieve value of userPassword
String password = null;
if (userPassword != null)
password = getAttributeValue(userPassword, attrs);
// Retrieve values of userRoleName attribute
ArrayList< String > roles = null;
if (userRoleName != null)
roles = addAttributeValues(userRoleName, attrs, roles);
return new User(username, dn, password, roles);
}
Use the UserPattern configuration attribute to
locate the directory entry for the user with the specified
username and return a User object; otherwise return
null. |
protected User getUserBySearch(DirContext context,
String username,
String[] attrIds) throws NamingException {
if (username == null || userSearchFormat == null)
return (null);
// Form the search filter
String filter = userSearchFormat.format(new String[] { username });
// Set up the search controls
SearchControls constraints = new SearchControls();
if (userSubtree) {
constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
}
else {
constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
}
// Specify the attributes to be retrieved
if (attrIds == null)
attrIds = new String[0];
constraints.setReturningAttributes(attrIds);
NamingEnumeration results =
context.search(userBase, filter, constraints);
// Fail if no entries found
if (results == null || !results.hasMore()) {
return (null);
}
// Get result for the first entry found
SearchResult result = (SearchResult)results.next();
// Check no further entries were found
if (results.hasMore()) {
if(containerLog.isInfoEnabled())
containerLog.info("username " + username + " has multiple entries");
return (null);
}
// Get the entry's distinguished name
NameParser parser = context.getNameParser("");
Name contextName = parser.parse(context.getNameInNamespace());
Name baseName = parser.parse(userBase);
// Bugzilla 32269
Name entryName = parser.parse(new CompositeName(result.getName()).get(0));
Name name = contextName.addAll(baseName);
name = name.addAll(entryName);
String dn = name.toString();
if (containerLog.isTraceEnabled())
containerLog.trace(" entry found for " + username + " with dn " + dn);
// Get the entry's attributes
Attributes attrs = result.getAttributes();
if (attrs == null)
return null;
// Retrieve value of userPassword
String password = null;
if (userPassword != null)
password = getAttributeValue(userPassword, attrs);
// Retrieve values of userRoleName attribute
ArrayList< String > roles = null;
if (userRoleName != null)
roles = addAttributeValues(userRoleName, attrs, roles);
return new User(username, dn, password, roles);
}
Search the directory to return a User object containing
information about the user with the specified username, if
found in the directory; otherwise return null. |
public String getUserPassword() {
return (this.userPassword);
}
Return the password attribute used to retrieve the user password. |
public String getUserPattern() {
return (this.userPattern);
}
Return the message format pattern for selecting users in this Realm. |
public String getUserRoleName() {
return userRoleName;
}
Return the user role name attribute name for this Realm. |
public String getUserSearch() {
return (this.userSearch);
}
Return the message format pattern for selecting users in this Realm. |
public boolean getUserSubtree() {
return (this.userSubtree);
}
Return the "search subtree for users" flag. |
protected DirContext open() throws NamingException {
// Do nothing if there is a directory server connection already open
if (context != null)
return (context);
try {
// Ensure that we have a directory context available
context = new InitialDirContext(getDirectoryContextEnvironment());
} catch (Exception e) {
connectionAttempt = 1;
// log the first exception.
containerLog.warn(sm.getString("jndiRealm.exception"), e);
// Try connecting to the alternate url.
context = new InitialDirContext(getDirectoryContextEnvironment());
} finally {
// reset it in case the connection times out.
// the primary may come back.
connectionAttempt = 0;
}
return (context);
}
Open (if necessary) and return a connection to the configured
directory server for this Realm. |
protected String[] parseUserPatternString(String userPatternString) {
if (userPatternString != null) {
ArrayList< String > pathList = new ArrayList< String >();
int startParenLoc = userPatternString.indexOf('(");
if (startParenLoc == -1) {
// no parens here; return whole thing
return new String[] {userPatternString};
}
int startingPoint = 0;
while (startParenLoc > -1) {
int endParenLoc = 0;
// weed out escaped open parens and parens enclosing the
// whole statement (in the case of valid LDAP search
// strings: (|(something)(somethingelse))
while ( (userPatternString.charAt(startParenLoc + 1) == '|") ||
(startParenLoc != 0 && userPatternString.charAt(startParenLoc - 1) == '\\") ) {
startParenLoc = userPatternString.indexOf("(", startParenLoc+1);
}
endParenLoc = userPatternString.indexOf(")", startParenLoc+1);
// weed out escaped end-parens
while (userPatternString.charAt(endParenLoc - 1) == '\\") {
endParenLoc = userPatternString.indexOf(")", endParenLoc+1);
}
String nextPathPart = userPatternString.substring
(startParenLoc+1, endParenLoc);
pathList.add(nextPathPart);
startingPoint = endParenLoc+1;
startParenLoc = userPatternString.indexOf('(", startingPoint);
}
return (String[])pathList.toArray(new String[] {});
}
return null;
}
Given a string containing LDAP patterns for user locations (separated by
parentheses in a pseudo-LDAP search string format -
"(location1)(location2)", returns an array of those paths. Real LDAP
search strings are supported as well (though only the "|" "OR" type). |
protected void release(DirContext context) {
; // NO-OP since we are not pooling anything
}
Release our use of this connection so that it can be recycled. |
public void setAlternateURL(String alternateURL) {
this.alternateURL = alternateURL;
}
Setter for property alternateURL. |
public void setAuthentication(String authentication) {
this.authentication = authentication;
}
Set the type of authentication to use. |
public void setConnectionName(String connectionName) {
this.connectionName = connectionName;
}
Set the connection username for this Realm. |
public void setConnectionPassword(String connectionPassword) {
this.connectionPassword = connectionPassword;
}
Set the connection password for this Realm. |
public void setConnectionURL(String connectionURL) {
this.connectionURL = connectionURL;
}
Set the connection URL for this Realm. |
public void setContextFactory(String contextFactory) {
this.contextFactory = contextFactory;
}
Set the JNDI context factory for this Realm. |
public void setDerefAliases(String derefAliases) {
this.derefAliases = derefAliases;
}
Set the value for derefAliases to be used when searching the directory. |
public void setProtocol(String protocol) {
this.protocol = protocol;
}
Set the protocol for this Realm. |
public void setReferrals(String referrals) {
this.referrals = referrals;
}
How do we handle JNDI referrals? ignore, follow, or throw
(see javax.naming.Context.REFERRAL for more information). |
public void setRoleBase(String roleBase) {
this.roleBase = roleBase;
}
Set the base element for role searches. |
public void setRoleName(String roleName) {
this.roleName = roleName;
}
Set the role name attribute name for this Realm. |
public void setRoleSearch(String roleSearch) {
this.roleSearch = roleSearch;
if (roleSearch == null)
roleFormat = null;
else
roleFormat = new MessageFormat(roleSearch);
}
Set the message format pattern for selecting roles in this Realm. |
public void setRoleSubtree(boolean roleSubtree) {
this.roleSubtree = roleSubtree;
}
Set the "search subtree for roles" flag. |
public void setUserBase(String userBase) {
this.userBase = userBase;
}
Set the base element for user searches. |
public void setUserPassword(String userPassword) {
this.userPassword = userPassword;
}
Set the password attribute used to retrieve the user password. |
public void setUserPattern(String userPattern) {
this.userPattern = userPattern;
if (userPattern == null)
userPatternArray = null;
else {
userPatternArray = parseUserPatternString(userPattern);
int len = this.userPatternArray.length;
userPatternFormatArray = new MessageFormat[len];
for (int i=0; i < len; i++) {
userPatternFormatArray[i] =
new MessageFormat(userPatternArray[i]);
}
}
}
Set the message format pattern for selecting users in this Realm.
This may be one simple pattern, or multiple patterns to be tried,
separated by parentheses. (for example, either "cn={0}", or
"(cn={0})(cn={0},o=myorg)" Full LDAP search strings are also supported,
but only the "OR", "|" syntax, so "(|(cn={0})(cn={0},o=myorg))" is
also valid. Complex search strings with &, etc are NOT supported. |
public void setUserRoleName(String userRoleName) {
this.userRoleName = userRoleName;
}
Set the user role name attribute name for this Realm. |
public void setUserSearch(String userSearch) {
this.userSearch = userSearch;
if (userSearch == null)
userSearchFormat = null;
else
userSearchFormat = new MessageFormat(userSearch);
}
Set the message format pattern for selecting users in this Realm. |
public void setUserSubtree(boolean userSubtree) {
this.userSubtree = userSubtree;
}
Set the "search subtree for users" flag. |
public void start() throws LifecycleException {
// Perform normal superclass initialization
super.start();
// Validate that we can open our connection
try {
open();
} catch (NamingException e) {
throw new LifecycleException(sm.getString("jndiRealm.open"), e);
}
}
Prepare for active use of the public methods of this Component. |
public void stop() throws LifecycleException {
// Perform normal superclass finalization
super.stop();
// Close any open directory server connection
close(this.context);
}
Gracefully shut down active use of the public methods of this Component. |