/* * 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(); } } } }