/*
* The MIT License
*
* Copyright (c) 2014, Stephen Connolly
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.model;
import hudson.DescriptorExtensionList;
import hudson.Extension;
import hudson.ExtensionPoint;
import hudson.model.AbstractDescribableImpl;
import hudson.util.CaseInsensitiveComparator;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundConstructor;
import javax.annotation.Nonnull;
import java.util.Comparator;
import java.util.Locale;
/**
* The strategy to use for manipulating converting names (e.g. user names, group names, etc) into ids.
*
* @since 1.566
*/
public abstract class IdStrategy extends AbstractDescribableImpl<IdStrategy> implements ExtensionPoint,
Comparator<String> {
/**
* The default case insensitive strategy.
*/
public static IdStrategy CASE_INSENSITIVE = new CaseInsensitive();
/**
* Converts an ID into a name that for use as a filename.
*
* @param id the id. Note, this method assumes that the id does not contain any filesystem unsafe characters.
* @return the name.
*/
@Nonnull
public abstract String filenameOf(@Nonnull String id);
/**
* Converts a filename into the corresponding id.
* @param filename the filename.
* @return the corresponding id.
* @since 1.577
*/
public String idFromFilename(@Nonnull String filename) {
return filename;
}
/**
* Converts an ID into a key for use in a Java Map.
*
* @param id the id.
* @return the key.
*/
@Nonnull
public abstract String keyFor(@Nonnull String id);
/**
* Compare two IDs and return {@code true} IFF the two ids are the same. Normally we expect that this should be
* the same as {@link #compare(String, String)} being equal to {@code 0}, however there may be a specific reason
* for going beyond that, such as sorting id's case insensitively while treating them as case sensitive.
*
* @param id1 the first id.
* @param id2 the second id.
* @return {@code true} if and only if the two ids are the same.
*/
public boolean equals(@Nonnull String id1, @Nonnull String id2) {
return compare(id1, id2) == 0;
}
/**
* Compare tow IDs and return their sorting order. If {@link #equals(String, String)} is {@code true} then this
* must return {@code 0} but {@link #compare(String, String)} returning {@code 0} need not imply that
* {@link #equals(String, String)} is {@code true}.
*
* @param id1 the first id.
* @param id2 the second id.
* @return the sorting order of the two IDs.
*/
@Override
public abstract int compare(@Nonnull String id1, @Nonnull String id2);
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
public IdStrategyDescriptor getDescriptor() {
return (IdStrategyDescriptor) super.getDescriptor();
}
/**
* This method is used to decide whether a {@link hudson.model.User#rekey()} operation is required.
*
* @param obj the object to compare with.
* @return {@code true} if and only if {@code this} is the same as {@code obj}.
*/
@Override
public boolean equals(Object obj) {
return this == obj || (obj != null && getClass().equals(obj.getClass()));
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return getClass().hashCode();
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return getClass().getName();
}
/**
* Returns all the registered {@link IdStrategy} descriptors.
*/
public static DescriptorExtensionList<IdStrategy, IdStrategyDescriptor> all() {
return Jenkins.getInstance().getDescriptorList(IdStrategy.class);
}
/**
* The default case insensitive {@link IdStrategy}
*/
public static class CaseInsensitive extends IdStrategy {
@DataBoundConstructor
public CaseInsensitive() {}
@Override
@Nonnull
public String filenameOf(@Nonnull String id) {
return id.toLowerCase(Locale.ENGLISH);
}
/**
* {@inheritDoc}
*/
@Override
@Nonnull
public String keyFor(@Nonnull String id) {
return id.toLowerCase(Locale.ENGLISH);
}
/**
* {@inheritDoc}
*/
@Override
public int compare(@Nonnull String id1, @Nonnull String id2) {
return CaseInsensitiveComparator.INSTANCE.compare(id1, id2);
}
@Extension @Symbol("caseInsensitive")
public static class DescriptorImpl extends IdStrategyDescriptor {
/**
* {@inheritDoc}
*/
@Override
public String getDisplayName() {
return Messages.IdStrategy_CaseInsensitive_DisplayName();
}
}
}
/**
* A case sensitive {@link IdStrategy}
*/
public static class CaseSensitive extends IdStrategy {
@DataBoundConstructor
public CaseSensitive() {}
/**
* {@inheritDoc}
*/
@Override
@Nonnull
public String filenameOf(@Nonnull String id) {
if (id.matches("[a-z0-9_. -]+")) {
return id;
} else {
StringBuilder buf = new StringBuilder(id.length() + 16);
for (char c : id.toCharArray()) {
if ('a' <= c && c <= 'z') {
buf.append(c);
} else if ('0' <= c && c <= '9') {
buf.append(c);
} else if ('_' == c || '.' == c || '-' == c || ' ' == c || '@' == c) {
buf.append(c);
} else if ('A' <= c && c <= 'Z') {
buf.append('~');
buf.append(Character.toLowerCase(c));
} else {
buf.append('$');
buf.append(StringUtils.leftPad(Integer.toHexString(c & 0xffff), 4, '0'));
}
}
return buf.toString();
}
}
@Override
public String idFromFilename(@Nonnull String filename) {
if (filename.matches("[a-z0-9_. -]+")) {
return filename;
} else {
StringBuilder buf = new StringBuilder(filename.length());
final char[] chars = filename.toCharArray();
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
if ('a' <= c && c <= 'z') {
buf.append(c);
} else if ('0' <= c && c <= '9') {
buf.append(c);
} else if ('_' == c || '.' == c || '-' == c || ' ' == c || '@' == c) {
buf.append(c);
} else if (c == '~') {
i++;
if (i < chars.length) {
buf.append(Character.toUpperCase(chars[i]));
}
} else if (c == '$') {
StringBuilder hex = new StringBuilder(4);
i++;
if (i < chars.length) {
hex.append(chars[i]);
} else {
break;
}
i++;
if (i < chars.length) {
hex.append(chars[i]);
} else {
break;
}
i++;
if (i < chars.length) {
hex.append(chars[i]);
} else {
break;
}
i++;
if (i < chars.length) {
hex.append(chars[i]);
} else {
break;
}
buf.append(Character.valueOf((char)Integer.parseInt(hex.toString(), 16)));
}
}
return buf.toString();
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(@Nonnull String id1, @Nonnull String id2) {
return StringUtils.equals(id1, id2);
}
/**
* {@inheritDoc}
*/
@Override
@Nonnull
public String keyFor(@Nonnull String id) {
return id;
}
/**
* {@inheritDoc}
*/
@Override
public int compare(@Nonnull String id1, @Nonnull String id2) {
return id1.compareTo(id2);
}
@Extension @Symbol("caseSensitive")
public static class DescriptorImpl extends IdStrategyDescriptor {
/**
* {@inheritDoc}
*/
@Override
public String getDisplayName() {
return Messages.IdStrategy_CaseSensitive_DisplayName();
}
}
}
/**
* A case sensitive email address {@link IdStrategy}. Providing this implementation among the set of default
* implementations as given the history of misunderstanding in the Jenkins code base around ID case sensitivity,
* if not provided people will get this wrong.
* <p/>
* Note: Not all email addresses are case sensitive. It is knowledge that belongs to the server that holds the
* mailbox. Most sane system administrators do not configure their accounts using case sensitive mailboxes
* but the RFC does allow them the option to configure that way. Domain names are always case insensitive per RFC.
*/
public static class CaseSensitiveEmailAddress extends CaseSensitive {
@DataBoundConstructor
public CaseSensitiveEmailAddress() {}
/**
* {@inheritDoc}
*/
@Override
@Nonnull
public String filenameOf(@Nonnull String id) {
return super.filenameOf(keyFor(id));
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(@Nonnull String id1, @Nonnull String id2) {
return StringUtils.equals(keyFor(id1), keyFor(id2));
}
/**
* {@inheritDoc}
*/
@Override
@Nonnull
public String keyFor(@Nonnull String id) {
int index = id.lastIndexOf('@'); // The @ can be used in local-part if quoted correctly
// => the last @ is the one used to separate the domain and local-part
return index == -1 ? id : id.substring(0, index) + (id.substring(index).toLowerCase(Locale.ENGLISH));
}
/**
* {@inheritDoc}
*/
@Override
public int compare(@Nonnull String id1, @Nonnull String id2) {
return keyFor(id1).compareTo(keyFor(id2));
}
@Extension
public static class DescriptorImpl extends IdStrategyDescriptor {
/**
* {@inheritDoc}
*/
@Override
public String getDisplayName() {
return Messages.IdStrategy_CaseSensitiveEmailAddress_DisplayName();
}
}
}
}