/*
 * The MIT License
 *
 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
 *
 * 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 hudson;

import hudson.model.Saveable;
import java.io.Closeable;
import java.io.IOException;

/**
 * Transaction-like object that can be used to make a bunch of changes to an object, and defer the
 * {@link Saveable#save()} until the end.
 *
 * <p>
 * The usage of {@link BulkChange} needs to follow a specific closure-like pattern, namely:
 *
 * <pre>
 * try (BulkChange bc = new BulkChange(someObject)) {
 *    ... make changes to 'someObject'
 *    bc.commit();
 * }
 * </pre>
 *
 * <p>
 * Use of this method is optional. If {@link BulkChange} is not used, individual mutator
 * will perform the save operation, and things will just run somewhat slower.
 *
 *
 * <h2>Cooperation from {@link Saveable}</h2>
 * <p>
 * For this class to work as intended, {@link Saveable} implementations need to co-operate.
 * Namely,
 *
 * <ol>
 * <li>
 * Mutator methods should invoke {@code this.save()} so that if the method is called outside
 * a {@link BulkChange}, the result will be saved immediately.
 *
 * <li>
 * In the {@code save()} method implementation, use {@link #contains(Saveable)} and
 * only perform the actual I/O operation when this method returns false.
 * </ol>
 *
 * <p>
 * See {@link jenkins.model.Jenkins#save()} as an example if you are not sure how to implement {@link Saveable}.
 *
 * @author Kohsuke Kawaguchi
 * @since 1.249
 */
public class BulkChange implements Closeable {
    private final Saveable saveable;
    public final Exception allocator;
    private final BulkChange parent;

    private boolean completed;

    public BulkChange(Saveable saveable) {
        this.parent = current();
        this.saveable = saveable;
        // remember who allocated this object in case
        // someone forgot to call save() at the end.
        allocator = new Exception();

        // in effect at construction
        INSCOPE.set(this);
    }

    /**
     * Saves the accumulated changes.
     */
    public void commit() throws IOException {
        if (completed)   return;
        completed = true;

        // move this object out of the scope first before save, or otherwise the save() method will do nothing.
        pop();
        saveable.save();
    }

    /**
     * Alias for {@link #abort()} to make {@link BulkChange} auto-closeable.
     */
    @Override
    public void close() {
        abort();
    }

    /**
     * Exits the scope of {@link BulkChange} without saving the changes.
     *
     * <p>
     * This can be used when a bulk change fails in the middle.
     * Note that unlike a real transaction, this will not roll back the state of the object.
     *
     * <p>
     * The abort method can be called after the commit method, in which case this method does nothing.
     * This is so that {@link BulkChange} can be used naturally in the try/finally block.
     */
    public void abort() {
        if (completed)   return;
        completed = true;
        pop();
    }

    private void pop() {
        if (current() != this)
            throw new AssertionError("Trying to save BulkChange that's not in scope");
        INSCOPE.set(parent);
    }

    /**
     * {@link BulkChange}s that are effective currently.
     */
    private static final ThreadLocal<BulkChange> INSCOPE = new ThreadLocal<>();

    /**
     * Gets the {@link BulkChange} instance currently in scope for the current thread.
     */
    public static BulkChange current() {
        return INSCOPE.get();
    }

    /**
     * Checks if the given {@link Saveable} is currently in the bulk change.
     *
     * <p>
     * The expected usage is from the {@link Saveable#save()} implementation to check
     * if the actual persistence should happen now or not.
     */
    public static boolean contains(Saveable s) {
        for (BulkChange b = current(); b != null; b = b.parent)
            if (b.saveable == s || b.saveable == ALL)
                return true;
        return false;
    }

    /**
     * Magic {@link Saveable} instance that can make {@link BulkChange} veto
     * all the save operations by making the {@link #contains(Saveable)} method return
     * true for everything.
     */
    public static final Saveable ALL = () -> {
    };
}
