/*
* The MIT License
*
* Copyright 2017 CloudBees, Inc.
*
* 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 com.cloudbees.hudson.plugins.folder;
import com.cloudbees.hudson.plugins.folder.computed.ComputedFolder;
import hudson.Util;
import hudson.model.AbstractItem;
import hudson.model.Action;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.JobProperty;
import hudson.model.TopLevelItem;
import java.io.Closeable;
import java.io.IOException;
import java.util.Map;
import java.util.WeakHashMap;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.model.TransientActionFactory;
/**
* Provides a way for a {@link ComputedFolder} to break the association between the directory names on disk
* that are used to store its items and the {@link Item#getName()} which is used to create the URL of the item.
* <p>
* <strong>NOTE:</strong> if you need to implement this functionality, you need to ensure that users cannot rename
* items within the {@link ComputedFolder} as renaming is not supported when using a {@link ChildNameGenerator}.
* <p>
* Challenges:
* <ul>
* <li>See the notes on {@link #itemNameFromItem(AbstractFolder, TopLevelItem)} and
* {@link #dirNameFromItem(AbstractFolder, TopLevelItem)} regarding the constraints on how to name things</li>
* <li>There are some items which need the {@link Item#getRootDir()} during construction (those are bold evil item types
* that leak side-effects, you should fix them if you find them). While you wait for them to be fixed you will need
* to work-arround the issue by ensuring that you call {@link #beforeCreateItem(AbstractFolder, String, String)}
* passing the {@link Item#getName()} you want the item to have <strong>and</strong> the ideal unmangled name
* <strong>before</strong> you call {@code new ChildItemType(parent,name)} and then call
* {@link #afterItemCreated(Trace)} when the constructor has returned. Then insure that your
* {@link #itemNameFromItem(AbstractFolder, TopLevelItem)} and {@link #dirNameFromItem(AbstractFolder, TopLevelItem)}
* fall back to {@link #idealNameFromItem(AbstractFolder, TopLevelItem)} when the magic property they are looking
* for is missing.</li>
* </ul>
*
* For a valid implementation, the
* {@link ComputedFolder} using this {@link ChildNameGenerator} will be attaching into the {@link Item} the
* actual name, typically via a {@link JobProperty} or {@link Action} (beware {@link TransientActionFactory}
* implementations may want to invoke {@link Item#getRootDir()} which will trigger a stack overflow though, so
* safer to stick with the {@link JobProperty} or equivalent). The
* {@link #itemNameFromItem(AbstractFolder, TopLevelItem)} method's task is to find the stored name
* and return the name stored within or {@code null} if that information is missing (in which case
* {@link #itemNameFromLegacy(AbstractFolder, String)} will be called to try and infer the name from the
* disk name that the {@link Item} is being loaded from.
* A similar relation exists for the {@link #dirNameFromItem(AbstractFolder, TopLevelItem)} and
* {@link #dirNameFromLegacy(AbstractFolder, String)} methods.
*
* @param <P> the type of {@link AbstractFolder}.
* @param <I> the type of {@link TopLevelItem} within the folder.
* @since 5.17
*/
// TODO migrate this functionality (by changing the base class) into core once baseline Jenkins has JENKINS-41222 merged
public abstract class ChildNameGenerator<P extends AbstractFolder<I>, I extends TopLevelItem> {
/**
* The name of the file that contains the actual name of the child item. This file is to allow a Jenkins
* Administrator to determine which child is which when dealing with a folder containing child names that have
* been mangled.
* <p>
* If there is nothing else to go on, this file will be used in preference to the child directory name, but as it
* is too easy for users to mistakenly think changing the contents of the file will rename the child (which could
* cause data loss for the computed folder's child) it is better for implementations to store the definitive
* ideal name in a {@link JobProperty}, {@link Action} or equivalent that is attached directly to the {@link Item}.
*/
public static final String CHILD_NAME_FILE = "name-utf8.txt";
private static final Map<Trace,String> idealNames = new WeakHashMap<Trace,String>();
/**
* Work-around helper method to "fix" {@link Item} constructors that have on-disk side-effects and therefore
* need {@link Item#getRootDir()} to work during the constructor.
* @param project the {@link AbstractFolder}.
* @param itemName the name that will be returned by {@link Item#getName()} when the item is constructed. This is
* the second parameter of {@link AbstractItem#AbstractItem(ItemGroup, String)}. This one would be
* the one with URL path segment escaping.
* @param idealName the original name before whatever URL path segment escaping you applied
* @return the {@link Trace} to keep track of when we can remove the memory of the creation process. Please
* {@link Trace#close()} the trace after the item is created.
*/
@Nonnull
public static final Trace beforeCreateItem(@Nonnull AbstractFolder<?> project,
@Nonnull String itemName,
@Nonnull String idealName) {
final Trace trace = new Trace(project, itemName);
synchronized (idealNames) {
idealNames.put(trace, idealName);
}
return trace;
}
/**
* Clean up for a creation {@link Trace}. Not strictly required, but nice implementations will do this via {@link Trace#close()}.
* @param trace the trace.
*/
private static final void afterItemCreated(@Nonnull Trace trace) {
synchronized (idealNames) {
idealNames.remove(trace);
}
}
/**
* Looks up the {@link Item} to see if we stored the ideal name before invoking the constructor that is having
* on-disk side-effects before the object has escaped {@link #beforeCreateItem(AbstractFolder, String, String)}
* @param parent the parent within which the item is being created.
* @param item the partially created item.
* @return the ideal name of the item.
*/
@CheckForNull
protected final String idealNameFromItem(@Nonnull P parent, @Nonnull I item) {
String itemName = item.getName();
if (itemName == null) {
return null;
}
synchronized (idealNames) {
return idealNames.get(new Trace(parent, itemName));
}
}
/**
* Infers the {@link Item#getName()} from the {@link Item} instance itself.
*
* Challenges include:
* <ul>
* <li>There are some characters that it would be really bad to return in the item name, such as
* {@code "/" / "?" / "#" / "[" / "]" / "\"} as these could end up modifying the effective URL</li>
* <li>There are names that it would be bad to return as the item name, such as
* {@code "" / "." / ".."} as these could end creating broken effective URLs</li>
* </ul>
* @param parent the parent within which the item is being loaded.
* @param item the partially loaded item (take care what methods you call, the item will not have a reference to
* its parent).
* @return the name of the item.
*/
@CheckForNull
public abstract String itemNameFromItem(@Nonnull P parent, @Nonnull I item);
/**
* Infers the directory name in which the {@link Item} instance itself should be stored.
*
* Challenges include:
* <ul>
* <li>The only really filesystem safe characters are {@code A-Za-z0-9_.-}</li>
* <li>Because of Windows and allowing for users to migrate their Jenkins from Unix to Windows and vice-versa,
* some names are reserved names under Windows:
* {@code AUX, COM1, COM2, ..., COM9, CON, LPT1, LPT2, ..., LPT9, NUL, PRN} plus all case variations of these
* names plus the variants where a single {@code .} is appended, you need to map those to something else</li>
* <li>Don't make the filenames too long. Try to keep them under 32 characters. If you can go smaller, even
* better.</li>
* <li>Get it right the first time</li>
* </ul>
*
* @param parent the parent within which the item is being loaded.
* @param item the partially loaded item (take care what methods you call, the item will not have a reference to
* its parent).
* @return the filesystem safe mangled equivalent name of the item.
*/
@CheckForNull
public abstract String dirNameFromItem(@Nonnull P parent, @Nonnull I item);
/**
* {@link #itemNameFromItem(AbstractFolder, TopLevelItem)} could not help, we are loading the item for the first
* time since the {@link ChildNameGenerator} was enabled for the parent folder type, this method's mission is
* to pretend the {@code legacyDirName} is the "mostly correct" name and turn this into the actual name.
*
* Challenges include:
* <ul>
* <li>Previously the name may have been over-encoded with {@link Util#rawEncode(String)} so you may need to
* decode it first</li>
* <li>There are some characters that it would be really bad to return in the item name, such as
* {@code "/" / "?" / "#" / "[" / "]" / "\"} as these could end up modifying the effective URL</li>
* <li>There are names that it would be bad to return as the item name, such as
* {@code "" / "." / ".."} as these could end creating broken effective URLs</li>
* </ul>
* @param parent the parent within which the item is being loaded.
* @param legacyDirName the directory name that we are loading an item from.
* @return the name of the item.
*/
@Nonnull
public abstract String itemNameFromLegacy(@Nonnull P parent, @Nonnull String legacyDirName);
/**
* {@link #dirNameFromItem(AbstractFolder, TopLevelItem)} could not help, we are loading the item for the first
* time since the {@link ChildNameGenerator} was enabled for the parent folder type, this method's mission is
* to pretend the {@code legacyDirName} is the "mostly correct" name and turn this into the filesystem safe
* mangled equivalent name to use going forward.
*
* Challenges include:
* <ul>
* <li>The only really filesystem safe characters are {@code A-Za-z0-9_.-}</li>
* <li>Because of Windows and allowing for users to migrate their Jenkins from Unix to Windows and vice-versa,
* some names are reserved names under Windows:
* {@code AUX, COM1, COM2, ..., COM9, CON, LPT1, LPT2, ..., LPT9, NUL, PRN} plus all case variations of these
* names plus the variants where a single {@code .} is appended, you need to map those to something else</li>
* <li>Don't make the filenames too long. Try to keep them under 32 characters. If you can go smaller, even
* better.</li>
* <li>Get it right the first time</li>
* </ul>
*
* @param parent the parent within which the item is being loaded.
* @param legacyDirName the directory name that we are loading an item from.
* @return the filesystem safe mangled equivalent name of the item.
*/
@Nonnull
public abstract String dirNameFromLegacy(@Nonnull P parent, @Nonnull String legacyDirName);
/**
* Record the ideal name inferred in the item when it was missing and has been inferred from the legacy directory
* name.
*
* @param parent the parent.
* @param item the item.
* @param legacyDirName the name of the directory that the item was loaded from.
* @throws IOException if the ideal name could not be attached to the item.
*/
public abstract void recordLegacyName(P parent, I item, String legacyDirName) throws IOException;
/**
* Traces the creation of a new {@link Item} in a folder. Use
* {@link ChildNameGenerator#beforeCreateItem(AbstractFolder, String, String)} to get the instance.
*/
public static final class Trace implements Closeable {
/**
* The folder.
*/
@Nonnull
private final AbstractFolder<?> folder;
/**
* The {@link Item#getName()} that we expect to be created in the very near future.
*/
@Nonnull
private final String itemName;
/**
* Constructor.
* @param folder the folder.
* @param itemName the item name
*/
private Trace(@Nonnull AbstractFolder<?> folder, @Nonnull String itemName) {
this.folder = folder;
this.itemName = itemName;
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Trace that = (Trace) o;
return folder == that.folder && itemName.equals(that.itemName);
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
int result = folder.hashCode();
result = 31 * result + itemName.hashCode();
return result;
}
/**
* {@inheritDoc}
*/
@Override
public void close() {
afterItemCreated(this);
}
}
}