/*
* ====================
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2008-2009 Sun Microsystems, Inc. All rights reserved.
*
* The contents of this file are subject to the terms of the Common Development
* and Distribution License("CDDL") (the "License"). You may not use this file
* except in compliance with the License.
*
* You can obtain a copy of the License at
* http://IdentityConnectors.dev.java.net/legal/license.txt
* See the License for the specific language governing permissions and limitations
* under the License.
*
* When distributing the Covered Code, include this CDDL Header Notice in each file
* and include the License file at identityconnectors/legal/license.txt.
* If applicable, add the following below this CDDL Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
* ====================
* "Portions Copyrighted 2014 ForgeRock AS"
*/
package org.identityconnectors.ldap.sync.sunds;
import static java.util.Collections.singletonList;
import static org.identityconnectors.common.CollectionUtil.isEmpty;
import static org.identityconnectors.common.CollectionUtil.newCaseInsensitiveMap;
import static org.identityconnectors.common.CollectionUtil.newCaseInsensitiveSet;
import static org.identityconnectors.common.CollectionUtil.newSet;
import static org.identityconnectors.common.CollectionUtil.nullAsEmpty;
import static org.identityconnectors.common.StringUtil.isBlank;
import static org.identityconnectors.ldap.LdapUtil.checkedListByFilter;
import static org.identityconnectors.ldap.LdapUtil.getStringAttrValue;
import static org.identityconnectors.ldap.LdapUtil.isUnderContexts;
import static org.identityconnectors.ldap.LdapUtil.nullAsEmpty;
import static org.identityconnectors.ldap.LdapUtil.quietCreateLdapName;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.naming.InvalidNameException;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapName;
import org.identityconnectors.common.Base64;
import org.identityconnectors.common.logging.Log;
import org.identityconnectors.common.security.GuardedString;
import org.identityconnectors.common.security.GuardedByteArray.Accessor;
import org.identityconnectors.framework.common.exceptions.ConnectorException;
import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.AttributeBuilder;
import org.identityconnectors.framework.common.objects.ConnectorObject;
import org.identityconnectors.framework.common.objects.ConnectorObjectBuilder;
import org.identityconnectors.framework.common.objects.ObjectClass;
import org.identityconnectors.framework.common.objects.OperationOptions;
import org.identityconnectors.framework.common.objects.OperationalAttributes;
import org.identityconnectors.framework.common.objects.SyncDelta;
import org.identityconnectors.framework.common.objects.SyncDeltaBuilder;
import org.identityconnectors.framework.common.objects.SyncDeltaType;
import org.identityconnectors.framework.common.objects.SyncResultsHandler;
import org.identityconnectors.framework.common.objects.SyncToken;
import org.identityconnectors.framework.common.objects.Uid;
import org.identityconnectors.framework.spi.SyncTokenResultsHandler;
import org.identityconnectors.ldap.LdapConnection;
import org.identityconnectors.ldap.LdapEntry;
import org.identityconnectors.ldap.search.DefaultSearchStrategy;
import org.identityconnectors.ldap.search.LdapFilter;
import org.identityconnectors.ldap.search.LdapInternalSearch;
import org.identityconnectors.ldap.search.LdapSearch;
import org.identityconnectors.ldap.search.LdapSearches;
import org.identityconnectors.ldap.search.LdapSearchResultsHandler;
import org.identityconnectors.ldap.sync.LdapSyncStrategy;
import org.identityconnectors.ldap.sync.sunds.LdifParser.ChangeSeparator;
import org.identityconnectors.ldap.sync.sunds.LdifParser.Line;
import org.identityconnectors.ldap.sync.sunds.LdifParser.NameValue;
import org.identityconnectors.ldap.sync.sunds.LdifParser.Separator;
/**
* An implementation of the sync operation based on the retro change log
* plugin of Sun Directory Server.
*/
public class SunDSChangeLogSyncStrategy implements LdapSyncStrategy {
// TODO detect that the change log has been trimmed.
private static final Log log = Log.getLog(SunDSChangeLogSyncStrategy.class);
/**
* The list of attribute operations supported by the "modify" LDIF change type.
*/
private static final Set<String> LDIF_MODIFY_OPS;
protected final LdapConnection conn;
private final ObjectClass oclass;
private ChangeLogAttributes changeLogAttrs;
private Set<String> oclassesToSync;
private Set<String> attrsToSync;
private PasswordDecryptor passwordDecryptor;
static {
LDIF_MODIFY_OPS = newCaseInsensitiveSet();
LDIF_MODIFY_OPS.add("add");
LDIF_MODIFY_OPS.add("delete");
LDIF_MODIFY_OPS.add("replace");
}
public SunDSChangeLogSyncStrategy(LdapConnection conn, ObjectClass oclass) {
this.conn = conn;
this.oclass = oclass;
}
public SyncToken getLatestSyncToken() {
return new SyncToken(getChangeLogAttributes().getLastChangeNumber());
}
public void sync(SyncToken token, final SyncResultsHandler handler, final OperationOptions options) {
String context = getChangeLogAttributes().getChangeLogContext();
final String changeNumberAttr = getChangeNumberAttribute();
SearchControls controls = LdapInternalSearch.createDefaultSearchControls();
controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
controls.setReturningAttributes(new String[] { changeNumberAttr, "targetDN", "changeType", "changes", "newRdn", "deleteOldRdn", "newSuperior", "targetEntryUUID", "targetUniqueID", "changeInitiatorsName" });
final int[] currentChangeNumber = { getStartChangeNumber(token) };
final int[] processedChangeNumber = { -1 };
final boolean[] results = new boolean[1];
do {
results[0] = false;
String filter = getChangeLogSearchFilter(changeNumberAttr, currentChangeNumber[0]);
LdapInternalSearch search = new LdapInternalSearch(conn, filter, singletonList(context), new DefaultSearchStrategy(false), controls);
search.execute(new LdapSearchResultsHandler() {
public boolean handle(String baseDN, SearchResult result) throws NamingException {
results[0] = true;
LdapEntry entry = LdapEntry.create(baseDN, result);
int changeNumber = convertToInt(getStringAttrValue(entry.getAttributes(), changeNumberAttr), -1);
if (changeNumber > currentChangeNumber[0]) {
currentChangeNumber[0] = changeNumber;
}
SyncDelta delta = createSyncDelta(entry, changeNumber, options.getAttributesToGet());
if (delta != null) {
boolean handled = handler.handle(delta);
if (handled){
processedChangeNumber[0] = changeNumber;
} else{
results[0] = false;
}
return handled;
}
processedChangeNumber[0] = changeNumber;
return true;
}
});
// We have already processed the current change.
// In the next cycle we want to start with the next change.
if (results[0]) {
currentChangeNumber[0]++;
}
} while (results[0]);
// ICF 1.4 now allows us to send the Token even if no entries were actually processed
if (processedChangeNumber[0] != -1){
((SyncTokenResultsHandler)handler).handleResult(new SyncToken(processedChangeNumber[0]));
}
}
private SyncDelta createSyncDelta(LdapEntry changeLogEntry, int changeNumber, String[] attrsToGetOption) {
log.ok("Attempting to create sync delta for log entry {0}", changeNumber);
String targetDN = getStringAttrValue(changeLogEntry.getAttributes(), "targetDN");
if (targetDN == null) {
log.error("Skipping log entry because it does not have a targetDN attribute");
return null;
}
LdapName targetName = quietCreateLdapName(targetDN);
if (filterOutByBaseContexts(targetName)) {
log.ok("Skipping log entry because it does not match any of the base contexts to synchronize");
return null;
}
String changeType = getStringAttrValue(changeLogEntry.getAttributes(), "changeType");
SyncDeltaType deltaType = getSyncDeltaType(changeType);
SyncDeltaBuilder syncDeltaBuilder = new SyncDeltaBuilder();
syncDeltaBuilder.setToken(new SyncToken(changeNumber));
syncDeltaBuilder.setDeltaType(deltaType);
if (deltaType.equals(SyncDeltaType.DELETE)) {
log.ok("Creating sync delta for deleted entry");
// XXX fix this!
if (filterOutDeleteByModifiersNames(changeLogEntry)) {
log.ok("Skipping entry because modifiersName is in the list of modifiersName's to filter out");
return null;
}
String uidAttr = conn.getSchemaMapping().getLdapUidAttribute(oclass);
if (!LdapEntry.isDNAttribute(uidAttr)) {
String guid = null;
if ("entryUUID".equalsIgnoreCase(uidAttr)) {
guid = getStringAttrValue(changeLogEntry.getAttributes(), "targetEntryUUID");
// dirty hack for now... See OPENICF-316
if (guid == null) {
guid = getStringAttrValue(changeLogEntry.getAttributes(), "targetUniqueID");
}
} else if ("nsUniqueId".equalsIgnoreCase(uidAttr)) {
// SunDS/ODSEE
guid = getStringAttrValue(changeLogEntry.getAttributes(), "targetUniqueID");
} else {
// try to see if we're not dealing with the RDN here...
String[] elements = targetDN.split(",");
String[] rdn = elements[0].split("=");
if (rdn[0].equalsIgnoreCase(uidAttr)){
guid = rdn[1];
}
}
if (null != guid) {
syncDeltaBuilder.setUid(new Uid(guid));
} else {
log.error("Failed to read the {0} attribute from Changelog entry.",uidAttr);
throw new ConnectorException("Unsupported Uid attribute: " + uidAttr);
}
} else {
syncDeltaBuilder.setUid(new Uid(targetDN));
}
return syncDeltaBuilder.build();
}
String changes = getStringAttrValue(changeLogEntry.getAttributes(), "changes");
Map<String, List<Object>> attrChanges = getAttributeChanges(changeType, changes);
if (filterOutByModifiersNames(attrChanges)) {
log.ok("Skipping entry because modifiersName is in the list of modifiersName's to filter out");
return null;
}
if (filterOutByAttributes(attrChanges)) {
log.ok("Skipping entry because no changed attributes in the list of attributes to synchronize");
return null;
}
// If the change type was modrdn, we need to compute the DN that the entry
// was modified to.
String newTargetDN = targetDN;
if ("modrdn".equalsIgnoreCase(changeType)) {
String newRdn = getStringAttrValue(changeLogEntry.getAttributes(), "newRdn");
if (isBlank(newRdn)) {
log.error("Skipping log entry because it does not have a newRdn attribute");
return null;
}
String newSuperior = getStringAttrValue(changeLogEntry.getAttributes(), "newSuperior");
newTargetDN = getNewTargetDN(targetName, newSuperior, newRdn);
}
// Always specify the attributes to get. This will return attributes with
// empty values when the attribute is not present, allowing the client to
// detect that the attribute has been removed.
Set<String> attrsToGet;
if (attrsToGetOption != null) {
attrsToGet = newSet(attrsToGetOption);
// Do not retrieve the password attribute from the entry (usually it is an unusable
// hashed value anyway). We will use the one from the change log below.
attrsToGet.remove(OperationalAttributes.PASSWORD_NAME);
} else {
attrsToGet = newSet(LdapSearch.getAttributesReturnedByDefault(conn, oclass));
}
// If objectClass is not in the list of attributes to get, prepare to remove it later.
boolean removeObjectClass = attrsToGet.add("objectClass");
LdapFilter filter = LdapFilter.forEntryDN(newTargetDN).withNativeFilter(getModifiedEntrySearchFilter());
ConnectorObject object = LdapSearches.findObject(conn, oclass, filter, attrsToGet.toArray(new String[attrsToGet.size()]));
if (object == null) {
log.ok("Skipping entry because the modified entry is missing, not of the right object class, or not matching the search filter");
return null;
}
Attribute oclassAttr = object.getAttributeByName("objectClass");
List<String> objectClasses = checkedListByFilter(nullAsEmpty(oclassAttr.getValue()), String.class);
if (filterOutByObjectClasses(objectClasses)) {
log.ok("Skipping entry because no object class in the list of object classes to synchronize");
return null;
}
Attribute passwordAttr = null;
if (conn.getConfiguration().isSynchronizePasswords()) {
List<Object> passwordValues = attrChanges.get(conn.getConfiguration().getPasswordAttributeToSynchronize());
if (!passwordValues.isEmpty()) {
byte[] encryptedPwd = (byte[]) passwordValues.get(0);
String decryptedPwd = getPasswordDecryptor().decryptPassword(encryptedPwd);
passwordAttr = AttributeBuilder.buildPassword(new GuardedString(decryptedPwd.toCharArray()));
}
}
if (removeObjectClass || passwordAttr != null) {
ConnectorObjectBuilder objectBuilder = new ConnectorObjectBuilder();
objectBuilder.setObjectClass(object.getObjectClass());
objectBuilder.setUid(object.getUid());
objectBuilder.setName(object.getName());
if (removeObjectClass) {
for (Attribute attr : object.getAttributes()) {
if (attr != oclassAttr) {
objectBuilder.addAttribute(attr);
}
}
} else {
objectBuilder.addAttributes(object.getAttributes());
}
if (passwordAttr != null) {
objectBuilder.addAttribute(passwordAttr);
}
object = objectBuilder.build();
}
log.ok("Creating sync delta for created or updated entry");
if ("modrdn".equalsIgnoreCase(changeType)) {
String uidAttr = conn.getSchemaMapping().getLdapUidAttribute(oclass);
// We can only set the previous Uid if it is the entry DN, which is readily available.
if (LdapEntry.isDNAttribute(uidAttr)) {
syncDeltaBuilder.setPreviousUid(conn.getSchemaMapping().createUid(oclass, targetDN));
}
}
syncDeltaBuilder.setUid(object.getUid());
syncDeltaBuilder.setObject(object);
return syncDeltaBuilder.build();
}
private String getNewTargetDN(LdapName targetName, String newSuperior, String newRdn) {
try {
LdapName newTargetName;
if (newSuperior == null) {
newTargetName = new LdapName(targetName.getRdns());
if (newTargetName.size() > 0) {
newTargetName.remove(targetName.size() - 1);
}
} else {
newTargetName = quietCreateLdapName(newSuperior);
}
newTargetName.add(newRdn);
return newTargetName.toString();
} catch (InvalidNameException e) {
throw new ConnectorException(e);
}
}
private boolean filterOutByBaseContexts(LdapName targetName) {
List<LdapName> baseContexts = conn.getConfiguration().getBaseContextsToSynchronizeAsLdapNames();
if (baseContexts.isEmpty()) {
baseContexts = conn.getConfiguration().getBaseContextsAsLdapNames();
}
if (!isUnderContexts(targetName, baseContexts)) {
return true;
}
return false;
}
protected boolean filterOutByModifiersNames(Map<String, List<Object>> changes) {
Set<LdapName> filter = conn.getConfiguration().getModifiersNamesToFilterOutAsLdapNames();
if (filter.isEmpty()) {
log.ok("Filtering by modifiersName disabled");
return false;
}
List<?> modifiersNameValues = changes.get("modifiersName");
if (isEmpty(modifiersNameValues)) {
log.ok("Not filtering by modifiersName because not set for this entry");
return false;
}
LdapName modifiersName = quietCreateLdapName(modifiersNameValues.get(0).toString());
return filter.contains(modifiersName);
}
private boolean filterOutDeleteByModifiersNames(LdapEntry changeLogEntry) {
Set<LdapName> filter = conn.getConfiguration().getModifiersNamesToFilterOutAsLdapNames();
if (filter.isEmpty()) {
log.ok("Filtering by modifiersName disabled");
return false;
}
String changeInitiatorsName = getStringAttrValue(changeLogEntry.getAttributes(), "changeInitiatorsName");
if (null == changeInitiatorsName) {
log.ok("Not filtering by changeInitiatorsName because not set for this entry");
return false;
}
LdapName modifiersName = quietCreateLdapName(changeInitiatorsName);
return filter.contains(modifiersName);
}
private boolean filterOutByAttributes(Map<String, List<Object>> attrChanges) {
Set<String> filter = getAttributesToSynchronize();
if (filter.isEmpty()) {
log.ok("Filtering by attributes disabled");
return false;
}
Set<String> changedAttrs = attrChanges.keySet();
return !containsAny(filter, changedAttrs);
}
private boolean filterOutByObjectClasses(List<String> objectClasses) {
Set<String> filter = getObjectClassesToSynchronize();
if (filter.isEmpty()) {
log.ok("Filtering by object class disabled");
return false;
}
return !containsAny(filter, objectClasses);
}
private SyncDeltaType getSyncDeltaType(String changeType) {
if ("delete".equalsIgnoreCase(changeType)) {
return SyncDeltaType.DELETE;
} else if ("modify".equalsIgnoreCase(changeType)){
return SyncDeltaType.UPDATE;
} else if ("add".equalsIgnoreCase(changeType)){
return SyncDeltaType.CREATE;
}
throw new IllegalArgumentException("Unknown change type: " + changeType);
}
private String getModifiedEntrySearchFilter() {
if (oclass.equals(ObjectClass.ACCOUNT)) {
return conn.getConfiguration().getAccountSynchronizationFilter();
} else if (oclass.equals(ObjectClass.GROUP)) {
return conn.getConfiguration().getGroupSynchronizationFilter();
}
return null;
}
private int getStartChangeNumber(SyncToken lastToken) {
Integer lastTokenValue = lastToken != null ? (Integer) lastToken.getValue() : null;
if (lastTokenValue == null) {
return getChangeLogAttributes().getFirstChangeNumber();
}
return lastTokenValue + 1; // Since the token value is the last value.
}
private Map<String, List<Object>> getAttributeChanges(String changeType, String ldif) {
Map<String, List<Object>> result = newCaseInsensitiveMap();
if ("modify".equalsIgnoreCase(changeType)) {
LdifParser parser = new LdifParser(ldif);
Iterator<Line> lines = parser.iterator();
while (lines.hasNext()) {
Line line = lines.next();
// We only expect one change, so ignore any change separators.
if (line instanceof Separator || line instanceof ChangeSeparator) {
continue;
}
NameValue nameValue = (NameValue) line;
String operation = nameValue.getName();
String attrName = nameValue.getValue();
if (LDIF_MODIFY_OPS.contains(operation)) {
List<Object> values = new ArrayList<Object>();
while (lines.hasNext()) {
line = lines.next();
if (line instanceof Separator) {
if ("delete".equalsIgnoreCase(operation)){
result.put(attrName, values);
}
break;
}
nameValue = (NameValue) line;
Object value = decodeAttributeValue(nameValue);
if (value != null) {
values.add(value);
}
}
if (!values.isEmpty()) {
result.put(attrName, values);
}
}
}
} else if ("add".equalsIgnoreCase(changeType)) {
LdifParser parser = new LdifParser(ldif);
Iterator<Line> lines = parser.iterator();
while (lines.hasNext()) {
Line line = lines.next();
// We only expect one change, so ignore any change separators.
if (line instanceof Separator || line instanceof ChangeSeparator) {
continue;
}
NameValue nameValue = (NameValue) line;
Object value = decodeAttributeValue(nameValue);
if (value != null) {
List<Object> values = result.get(nameValue.getName());
if (values == null) {
values = new ArrayList<Object>();
result.put(nameValue.getName(), values);
}
values.add(value);
}
}
}
// Returning an empty map when changeType is "delete" or "modrdn".
return result;
}
private Object decodeAttributeValue(NameValue nameValue) {
String value = nameValue.getValue();
if (value.startsWith(":")) {
// This is a Base64 encoded value...
String base64 = value.substring(1).trim();
try {
return Base64.decode(base64);
// TODO the adapter had code here to convert the byte array
// to a string if the attribute was of a string type. Since
// that information is in the schema and we don't have access
// to the resource schema, leaving that functionality out for now.
} catch (Exception e) {
log.error("Could not decode attribute {0} with Base64 value {1}", nameValue.getName(), nameValue.getValue());
return null;
}
} else {
return value;
}
}
private String getChangeLogSearchFilter(String changeNumberAttr, int startChangeNumber) {
int blockSize = conn.getConfiguration().getChangeLogBlockSize();
boolean filterWithOrInsteadOfAnd = conn.getConfiguration().isFilterWithOrInsteadOfAnd();
boolean filterByLogEntryOClass = !conn.getConfiguration().isRemoveLogEntryObjectClassFromFilter();
StringBuilder result = new StringBuilder();
if (filterWithOrInsteadOfAnd) {
if (filterByLogEntryOClass) {
result.append("(&(objectClass=changeLogEntry)");
}
result.append("(|(");
result.append(changeNumberAttr);
result.append('=');
result.append(startChangeNumber);
result.append(')');
int endChangeNumber = startChangeNumber + blockSize -1;
for (int i = startChangeNumber + 1; i <= endChangeNumber; i++) {
result.append("(");
result.append(changeNumberAttr);
result.append('=');
result.append(i);
result.append(')');
}
result.append(')');
if (filterByLogEntryOClass) {
result.append(')');
}
} else {
result.append("(&");
if (filterByLogEntryOClass) {
result.append("(objectClass=changeLogEntry)");
}
result.append("(");
result.append(changeNumberAttr);
result.append(">=");
result.append(startChangeNumber);
result.append(')');
int endChangeNumber = startChangeNumber + blockSize -1;
result.append("(");
result.append(changeNumberAttr);
result.append("<=");
result.append(endChangeNumber);
result.append(')');
result.append(')');
}
return result.toString();
}
ChangeLogAttributes getChangeLogAttributes() {
if (changeLogAttrs == null) {
try {
Attributes attrs = conn.getInitialContext().getAttributes("", new String[] { "changeLog", "firstChangeNumber", "lastChangeNumber" });
String changeLog = getStringAttrValue(attrs, "changeLog");
String firstChangeNumber = getStringAttrValue(attrs, "firstChangeNumber");
String lastChangeNumber = getStringAttrValue(attrs, "lastChangeNumber");
if (changeLog == null || firstChangeNumber == null | lastChangeNumber == null) {
String error = "Unable to locate the replication change log.\n"+
"From the admin console please verify that the "+
"change log is enabled under Configuration: "+
"Replication: Supplier Settings and that the Retro "+
"Change Log Plugin is enabled under Configuration: "+
"Plug-ins: Retro Change Log Plugin";
throw new ConnectorException(error);
}
changeLogAttrs = new ChangeLogAttributes(changeLog, convertToInt(firstChangeNumber, 0), convertToInt(lastChangeNumber, 0));
} catch (NamingException e) {
throw new ConnectorException(e);
}
}
return changeLogAttrs;
}
private String getChangeNumberAttribute() {
String result = conn.getConfiguration().getChangeNumberAttribute();
if (isBlank(result)) {
result = "changeNumber";
}
return result;
}
private Set<String> getAttributesToSynchronize() {
if (attrsToSync == null) {
Set<String> result = newCaseInsensitiveSet();
result.addAll(Arrays.asList(nullAsEmpty(conn.getConfiguration().getAttributesToSynchronize())));
if (conn.getConfiguration().isSynchronizePasswords()) {
result.add(conn.getConfiguration().getPasswordAttributeToSynchronize());
}
attrsToSync = result;
}
return attrsToSync;
}
private Set<String> getObjectClassesToSynchronize() {
if (oclassesToSync == null) {
Set<String> result = newCaseInsensitiveSet();
result.addAll(Arrays.asList(nullAsEmpty(conn.getConfiguration().getObjectClassesToSynchronize())));
oclassesToSync = result;
}
return oclassesToSync;
}
private PasswordDecryptor getPasswordDecryptor() {
if (passwordDecryptor == null) {
conn.getConfiguration().getPasswordDecryptionKey().access(new Accessor() {
public void access(final byte[] decryptionKey) {
conn.getConfiguration().getPasswordDecryptionInitializationVector().access(new Accessor() {
public void access(byte[] decryptionIV) {
passwordDecryptor = new PasswordDecryptor(decryptionKey, decryptionIV);
}
});
}
});
}
assert passwordDecryptor != null;
return passwordDecryptor;
}
private boolean containsAny(Set<String> haystack, Collection<String> needles) {
for (String needle : needles) {
if (haystack.contains(needle)) {
return true;
}
}
return false;
}
public static int convertToInt(String number, int def) {
int result = def;
if (number != null && number.length() > 0) {
int decimal = number.indexOf('.');
if (decimal > 0) {
number = number.substring(0, decimal);
}
try {
result = Integer.parseInt(number);
} catch (NumberFormatException e) {
// Ignore.
}
}
return result;
}
}