package com.mikepenz.crossfadedrawerlayout.view;

import android.content.Context;
import androidx.core.view.ViewCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;

import com.mikepenz.crossfadedrawerlayout.ApplyTransformationListener;
import com.mikepenz.crossfadedrawerlayout.animation.ResizeWidthAnimation;
import com.mikepenz.materialize.util.UIUtils;
import com.mikepenz.materialize.view.ScrimInsetsRelativeLayout;

/**
 * Created by mikepenz on 20.10.15.
 */
public class CrossfadeDrawerLayout extends DrawerLayout {
    private static final int DEFAULT_ANIMATION = 200;

    private boolean mDrawerOpened = false;

    private float mTouchDown = -1;
    private float mPrevTouch = -1;

    private CrossfadeListener mCrossfadeListener;

    private int mMinWidth = 0;
    private int mMaxWidth = 0;

    private ScrimInsetsRelativeLayout mContainer;
    private ViewGroup mSmallView;
    private ViewGroup mLargeView;

    private boolean mIsCrossfaded = false;

    private boolean mSliderOnRight = false;

    public CrossfadeDrawerLayout(Context context) {
        super(context);
        init(context);
    }

    public CrossfadeDrawerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public CrossfadeDrawerLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    public void init(Context ctx) {
        super.addDrawerListener(drawerListener);
        //define default valuse for min and max
        mMinWidth = (int) UIUtils.convertDpToPixel(72, ctx);
        mMaxWidth = (int) UIUtils.convertDpToPixel(200, ctx);
    }

    public void setMinWidthPx(int minWidth) {
        this.mMinWidth = minWidth;
    }

    public void setMaxWidthPx(int maxWidth) {
        this.mMaxWidth = maxWidth;
    }

    public ViewGroup getSmallView() {
        return mSmallView;
    }

    public ViewGroup getLargeView() {
        return mLargeView;
    }

    public ScrimInsetsRelativeLayout getContainer() {
        return mContainer;
    }

    public boolean isCrossfaded() {
        return mIsCrossfaded;
    }

    public CrossfadeDrawerLayout withSliderOnRight(boolean mSliderOnRight) {
        this.mSliderOnRight = mSliderOnRight;
        return this;
    }

    /**
     * defines a CrossfadeListener which is called when you slide the crossfader
     *
     * @param crossfadeListener
     * @return
     */
    public CrossfadeDrawerLayout withCrossfadeListener(CrossfadeListener crossfadeListener) {
        this.mCrossfadeListener = crossfadeListener;
        return this;
    }

    @Override
    public void addView(View child, int index) {
        child = wrapSliderContent(child, index);
        super.addView(child, index);
    }

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        child = wrapSliderContent(child, index);
        super.addView(child, index, params);
    }

    /**
     * this will wrap the view which is added to the slider into another layout so we can then overlap the small and large view
     *
     * @param child
     * @param index
     * @return
     */
    private View wrapSliderContent(View child, int index) {
        //TODO !!
        if (index == 1 && child.getId() != -1) {
            mLargeView = (ViewGroup) child;
            mContainer = new ScrimInsetsRelativeLayout(getContext());
            mContainer.setGravity(Gravity.START);
            mContainer.setLayoutParams(child.getLayoutParams());

            mContainer.addView(mLargeView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);

            mSmallView = new LinearLayout(getContext());
            mContainer.addView(mSmallView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);

            mLargeView.setAlpha(0);
            mLargeView.setVisibility(View.GONE);

            //correct fitsSystemWindows handling
            mContainer.setFitsSystemWindows(true);
            mSmallView.setFitsSystemWindows(true);

            return mContainer;
        }
        return child;
    }

    public DrawerListener drawerListener = new DrawerListener() {
        @Override
        public void onDrawerSlide(View drawerView, float slideOffset) {
            mDrawerOpened = slideOffset == 1;
        }

        @Override
        public void onDrawerOpened(View drawerView) {
            mDrawerOpened = true;
        }

        @Override
        public void onDrawerClosed(View drawerView) {
            MarginLayoutParams lp = (MarginLayoutParams) drawerView.getLayoutParams();
            lp.width = mMinWidth;
            drawerView.setLayoutParams(lp);

            //revert alpha :D
            mSmallView.setAlpha(1);
            mSmallView.bringToFront();
            mLargeView.setAlpha(0);
            mDrawerOpened = false;
        }

        @Override
        public void onDrawerStateChanged(int newState) {
        }
    };

    @Override
    public boolean dispatchTouchEvent(MotionEvent motionEvent) {
        if (mDrawerOpened) {
            if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
                mTouchDown = motionEvent.getX();
                mPrevTouch = motionEvent.getX();
                return super.dispatchTouchEvent(motionEvent);
            } else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
                boolean click = mTouchDown == mPrevTouch;
                mTouchDown = -1;
                mPrevTouch = -1;

                MarginLayoutParams lp = (MarginLayoutParams) mContainer.getLayoutParams();
                float percentage = calculatePercentage(lp.width);
                if (percentage > 50) {
                    fadeUp(DEFAULT_ANIMATION);
                } else {
                    fadeDown(DEFAULT_ANIMATION);
                }

                if (click) {
                    return super.dispatchTouchEvent(motionEvent);
                } else {
                    return true;
                }
            } else if (motionEvent.getAction() == MotionEvent.ACTION_MOVE && mTouchDown != -1) {
                MarginLayoutParams lp = (MarginLayoutParams) mContainer.getLayoutParams();
                //the current drawer width
                float diff = motionEvent.getX() - mTouchDown;

                if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL || mSliderOnRight) {
                    diff = diff * (-1);
                }

                if (diff == 0) {
                    //no difference nothing to do
                    //return super.dispatchTouchEvent(motionEvent);
                } else if (diff > 0 && lp.width <= mMaxWidth && (lp.width + diff) < mMaxWidth && lp.width >= mMinWidth) {
                    lp.width = (int) (lp.width + diff);
                    mContainer.setLayoutParams(lp);
                    mTouchDown = motionEvent.getX();
                    overlapViews(lp.width);
                } else if (diff < 0 && lp.width >= mMinWidth && (lp.width + diff) > mMinWidth) {
                    lp.width = (int) (lp.width + diff);
                    mContainer.setLayoutParams(lp);
                    mTouchDown = motionEvent.getX();
                    overlapViews(lp.width);
                } else if (lp.width < mMinWidth) {
                    lp.width = mMinWidth;
                    mContainer.setLayoutParams(lp);
                    mDrawerOpened = false;
                    mTouchDown = -1;
                    overlapViews(mMinWidth);
                } else if ((lp.width + diff) < mMinWidth) {
                    //return super.dispatchTouchEvent(motionEvent);
                }
                //return true;
            }
        }
        return super.dispatchTouchEvent(motionEvent);
    }

    @Override
    public boolean onTouchEvent(MotionEvent motionEvent) {
        try {
            return super.onTouchEvent(motionEvent);
        } catch (RuntimeException ex) {
            ex.printStackTrace();
        }
        return false;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        try {
            return super.onInterceptTouchEvent(ev);
        } catch (IllegalArgumentException ex) {
            ex.printStackTrace();
        }
        return false;
    }

    @Override
    public void openDrawer(int gravity) {
        mDrawerOpened = true;
        super.openDrawer(gravity);
    }

    @Override
    public void openDrawer(View drawerView) {
        mDrawerOpened = true;
        super.openDrawer(drawerView);
    }

    @Override
    public void openDrawer(View drawerView, boolean animate) {
        mDrawerOpened = true;
        super.openDrawer(drawerView, animate);
    }

    @Override
    public void openDrawer(int gravity, boolean animate) {
        mDrawerOpened = true;
        super.openDrawer(gravity, animate);
    }

    @Override
    public void closeDrawer(View drawerView) {
        mDrawerOpened = false;
        mContainer.clearAnimation();
        super.closeDrawer(drawerView);
    }

    @Override
    public void closeDrawer(View drawerView, boolean animate) {
        mDrawerOpened = false;
        mContainer.clearAnimation();
        super.closeDrawer(drawerView, animate);
    }

    @Override
    public void closeDrawer(int gravity) {
        mDrawerOpened = false;
        mContainer.clearAnimation();
        super.closeDrawer(gravity);
    }

    @Override
    public void closeDrawer(int gravity, boolean animate) {
        mDrawerOpened = false;
        mContainer.clearAnimation();
        super.closeDrawer(gravity, animate);
    }

    /**
     * crossfade the small to the large view (with default animation time)
     */
    public void crossfade() {
        crossfade(DEFAULT_ANIMATION);
    }

    /**
     * crossfade the small to the large view
     *
     * @param duration
     */
    public void crossfade(int duration) {
        if (isCrossfaded()) {
            fadeDown(duration);
        } else {
            fadeUp(duration);
        }
    }

    /**
     * animate to the large view
     *
     * @param duration
     */
    public void fadeUp(int duration) {
        //animate up
        mContainer.clearAnimation();
        ResizeWidthAnimation anim = new ResizeWidthAnimation(mContainer, mMaxWidth, new ApplyTransformationListener() {
            @Override
            public void applyTransformation(int width) {
                overlapViews(width);
            }
        });
        anim.setDuration(duration);
        mContainer.startAnimation(anim);
    }

    /**
     * animate to the small view
     *
     * @param duration
     */
    public void fadeDown(int duration) {
        //fade down
        mContainer.clearAnimation();
        ResizeWidthAnimation anim = new ResizeWidthAnimation(mContainer, mMinWidth, new ApplyTransformationListener() {
            @Override
            public void applyTransformation(int width) {
                overlapViews(width);
            }
        });
        anim.setDuration(duration);
        mContainer.startAnimation(anim);
    }

    /**
     * calculate the percentage to how many percent the slide is already visible
     *
     * @param width
     * @return
     */
    private float calculatePercentage(int width) {
        int absolute = mMaxWidth - mMinWidth;
        int current = width - mMinWidth;
        float percentage = 100.0f * current / absolute;
        //we can assume that we are crossfaded if the percentage is > 90
        mIsCrossfaded = percentage > 90;
        return percentage;
    }

    //remember the previous width to optimize performance
    private int mWidth = -1;

    /**
     * overlap the views and provide the crossfade effect
     *
     * @param width
     */
    private void overlapViews(int width) {
        if (width == mWidth) {
            return;
        }
        //remember this width so it is't processed twice
        mWidth = width;

        float percentage = calculatePercentage(width);
        float alpha = percentage / 100;

        mSmallView.setAlpha(1);
        mSmallView.setClickable(false);
        mLargeView.bringToFront();
        mLargeView.setAlpha(alpha);
        mLargeView.setClickable(true);
        mLargeView.setVisibility(alpha > 0.01f ? View.VISIBLE : View.GONE);

        //notify the crossfadeListener
        if (mCrossfadeListener != null) {
            mCrossfadeListener.onCrossfade(mContainer, calculatePercentage(width), width);
        }
    }

    public interface CrossfadeListener {
        void onCrossfade(View containerView, float currentSlidePercentage, int slideOffset);
    }
}


/*
 * Copyright (C) 2015 Andrew Reitz
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.andrewreitz.spock.android.extension;

import android.app.Activity;
import android.app.Instrumentation;
import android.content.Intent;
import android.support.test.InstrumentationRegistry;
import com.andrewreitz.spock.android.BundleCreator;
import com.andrewreitz.spock.android.ActivityRunMode;

import org.spockframework.runtime.extension.AbstractMethodInterceptor;
import org.spockframework.runtime.extension.IMethodInvocation;
import org.spockframework.runtime.model.FieldInfo;

/**
 * Intercepts setup fixtures to instantiate an activity.
 *
 * @author Andrew Reitz
 * @since 1.1
 */
public class UseActivityInterceptor extends AbstractMethodInterceptor {
  private final FieldInfo fieldInfo;
  private final Class<? extends Activity> activityClass;
  private final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();

  private Activity activity;
  private BundleCreator bundleCreator;
  private ActivityRunMode activityRunMode;

  /**
   * Constructor
   *
   * @param fieldInfo The field that should be instantiated.
   * @param activityClass The activity class type that should be created and set the annotated
   * field
   * to.
   * @param bundleCreator The bundle creator that will be used to pass a bundle to the started
   * activity.
   */
  public UseActivityInterceptor(FieldInfo fieldInfo, Class<? extends Activity> activityClass,
      BundleCreator bundleCreator, ActivityRunMode activityRunMode) {
    this.fieldInfo = fieldInfo;
    this.activityClass = activityClass;
    this.bundleCreator = bundleCreator;
    this.activityRunMode = activityRunMode;
  }

  @Override public void interceptSetupMethod(IMethodInvocation invocation) throws Throwable {
    fieldInfo.writeValue(invocation.getInstance(), getActivity());
    invocation.proceed();
  }

  /**
   * The intent to launch the Activity.
   *
   * @param targetPackage The package of the Activity.
   * @param activityClass The Activity class send the intent to.
   * @param bundleCreator Bundle creator instance that will give a bundle to the activity.
   * @param <T> The specific Activity type.
   * @return The intent to start the activity.
   */
  protected <T extends Activity> Intent getLaunchIntent(String targetPackage,
      Class<T> activityClass, BundleCreator bundleCreator) {
    Intent intent = new Intent(Intent.ACTION_MAIN);
    intent.setClassName(targetPackage, activityClass.getName());
    intent.putExtras(bundleCreator.createBundle());
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    return intent;
  }

  /**
   * @return Launches the activity if it hasn't been started, then returns it.
   */
  private Activity getActivity() {
    launchActivity();
    return activity;
  }

  /** Launch the activity if needed. */
  @SuppressWarnings("unchecked")
  private void launchActivity() {
    if (activity != null && ActivityRunMode.SPECIFICATION.equals(activityRunMode)) return;

    String targetPackage = instrumentation.getTargetContext().getPackageName();
    Intent intent = getLaunchIntent(targetPackage, activityClass, bundleCreator);

    activity = instrumentation.startActivitySync(intent);
    instrumentation.waitForIdleSync();
  }
}


/*
 * Copyright 2016-2018 MarkLogic Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.marklogic.semantics.jena;

import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Locale;

import org.apache.jena.graph.Triple;
import org.apache.jena.sparql.core.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.jena.graph.Graph;
import org.apache.jena.graph.Node;
import org.apache.jena.graph.NodeFactory;
import org.apache.jena.query.Dataset;
import org.apache.jena.query.DatasetFactory;
import org.apache.jena.query.ReadWrite;
import org.apache.jena.shared.Lock;
import org.apache.jena.shared.LockNone;
import com.marklogic.client.io.InputStreamHandle;
import com.marklogic.client.query.QueryDefinition;
import com.marklogic.client.semantics.GraphPermissions;
import com.marklogic.client.semantics.RDFTypes;
import com.marklogic.client.semantics.SPARQLBindings;
import com.marklogic.client.semantics.SPARQLQueryDefinition;
import com.marklogic.client.semantics.SPARQLRuleset;
import com.marklogic.semantics.jena.client.JenaDatabaseClient;
import com.marklogic.semantics.jena.client.QuadsIterator;
import com.marklogic.semantics.jena.client.WrappingIterator;

/**
 * A representation of MarkLogic's triple store as a DatasetGraph, plus a few
 * extra MarkLogic-specific features. Use this class as you would any other
 * DatasetGraph in a jena-based project. If you know you are using a
 * MarkLogicDatasetGraph, simply cast it in order to use the MarkLogic-specific
 * capabilities.
 *
 * Extending DatasetGraphTriplesQuads means
 * we need only implement four methods
 *
 * protected abstract void addToDftGraph(Node s, Node p, Node o) ;
 * protected abstract void addToNamedGraph(Node g, Node s, Node p, Node o) ;
 * protected abstract void deleteFromDftGraph(Node s, Node p, Node o) ;
 * protected abstract void deleteFromNamedGraph(Node g, Node s, Node p, Node o) ;
 */
public class MarkLogicDatasetGraph extends DatasetGraphTriplesQuads implements
        DatasetGraph, Transactional {

    public static final String DEFAULT_GRAPH_URI = "http://marklogic.com/semantics#default-graph";
    private static Logger log = LoggerFactory
            .getLogger(MarkLogicDatasetGraph.class);

    private JenaDatabaseClient client;

    /*
     * An inferencing datasetGraph will add ruleset config to each query
     */
    private SPARQLRuleset[] rulesets;

    /*
     * Used to constrain of corpus of documents against which to make a Dataset
     */
    private QueryDefinition constrainingQueryDefinition;
    private GraphPermissions updatePermissions;

    /**
     * Creates a new MarkLogicDatasetGraph using the supplied DatabaseClient. If
     * this client can write to the database, then the DatasetGraph is
     * initialized with a default graph.
     *
     * MarkLogicDatasetGraph is a long-lived object.  Use the same one for the
     * duration of your session.
     *
     * @param jenaClient
     *            specifies the connection to the MarkLogic server. Obtain from
     *            DatabaseClientFactory.
     */
    public MarkLogicDatasetGraph(JenaDatabaseClient jenaClient) {
        this.client = jenaClient;
    }

    /**
     * @see org.apache.jena.sparql.core.DatasetGraph
     */
    @Override
    public Iterator<Node> listGraphNodes() {
        log.debug("listing graphs ");
        checkIsOpen();
        sync();
        Iterator<String> graphNames = client.listGraphUris();
        return new WrappingIterator(graphNames);
    }

    /**
     * MarkLogicDatasetGraph does not make use of locks.
     *
     * @return An instance of org.apache.jena.shared.LockNone
     */
    @Override
    public Lock getLock() {
        return new LockNone();
    }

    /**
     * @see org.apache.jena.sparql.core.DatasetGraph
     */
    @Override
    public void clear() {
        checkIsOpen();
        String query = "DROP SILENT ALL";
        SPARQLQueryDefinition qdef = client.newQueryDefinition(query);
        client.executeUpdate(qdef);
    }

    /**
     * Maps Jena bindings defined by a variable name and a
     * {@link org.apache.jena.graph.Node} to MarkLogic
     * {@link com.marklogic.client.semantics.SPARQLQueryDefinition} bindings.
     *
     * @param qdef
     *            A {@link com.marklogic.client.semantics.SPARQLQueryDefinition}
     *            to decorate with a binding.
     * @param variableName
     *            Name of the variable
     * @param objectNode
     *            An RDF node with the value of variableName.
     * @return The query definition, with binding set.
     */
    public static SPARQLQueryDefinition bindObject(SPARQLQueryDefinition qdef,
            String variableName, Node objectNode) {
        SPARQLBindings bindings = qdef.getBindings();
        if (objectNode.isURI()) {
            bindings.bind(variableName, objectNode.getURI());
        } else if (objectNode.isLiteral()) {
            if (! "".equals(objectNode.getLiteralLanguage())) {
              String languageTag = objectNode.getLiteralLanguage();
              bindings.bind(variableName, objectNode.getLiteralLexicalForm(),
                      Locale.forLanguageTag(languageTag));
            } else if (objectNode.getLiteralDatatype() != null) {
                try {
                    String xsdType = objectNode.getLiteralDatatypeURI();
                    String fragment = new URI(xsdType).getFragment();
                    bindings.bind(variableName,
                            objectNode.getLiteralLexicalForm(),
                            RDFTypes.valueOf(fragment.toUpperCase()));
                } catch (URISyntaxException e) {
                    throw new MarkLogicJenaException(
                            "Unrecognized binding type.  Use XSD only.", e);
                }
            } else {
                // is this a hole, no type string?
                bindings.bind(variableName, objectNode.getLiteralLexicalForm(),
                        RDFTypes.STRING);
            }
        }
        qdef.setBindings(bindings);
        return qdef;
    }

    /*
     * @see org.apache.jena.sparql.core.DatasetGraphTriplesQuads Internally uses
     * a write cache to hold batches of quad updates. @see sync()
     */
    @Override
    protected void addToDftGraph(Node s, Node p, Node o) {
        checkIsOpen();
        Node s1 = skolemize(s);
        Node p1 = skolemize(p);
        Node o1 = skolemize(o);
        client.sinkQuad(null, s1, p1, o1);
    }

    private Node skolemize(Node s) {
        if (s.isBlank()) {
            return NodeFactory
                    .createURI("http://marklogic.com/semantics/blank/"
                            + s.toString());
        } else {
            return s;
        }
    }

    /**
     * @see org.apache.jena.sparql.core.DatasetGraphTriplesQuads Internally uses
     *      a write cache to hold batches of quad updates. @see sync()
     */
    @Override
    protected void addToNamedGraph(Node g, Node s, Node p, Node o) {
        checkIsOpen();
        Node s1 = skolemize(s);
        Node p1 = skolemize(p);
        Node o1 = skolemize(o);
        client.sinkQuad(g, s1, p1, o1);
    }

    /**
     * @see org.apache.jena.sparql.core.DatasetGraphTriplesQuads
     */
    @Override
    protected void deleteFromDftGraph(Node s, Node p, Node o) {
        checkIsOpen();
        syncAdds();
        Node s1 = skolemize(s);
        Node p1 = skolemize(p);
        Node o1 = skolemize(o);
        client.sinkDelete(null, s1, p1, o1);
    }

    /**
     * @see org.apache.jena.sparql.core.DatasetGraphTriplesQuads
     */
    @Override
    protected void deleteFromNamedGraph(Node g, Node s, Node p, Node o) {
        checkIsOpen();
        syncAdds();
        Node s1 = skolemize(s);
        Node p1 = skolemize(p);
        Node o1 = skolemize(o);
        client.sinkDelete(g, s, p, o);
    }

    private InputStream selectTriplesInGraph(String graphName, Node s, Node p,
            Node o) {
        checkIsOpen();
        Node s1 = s != null ? s : Node.ANY;
        Node p1 = p != null ? p : Node.ANY;
        Node o1 = o != null ? o : Node.ANY;
        Node s2 = skolemize(s1);
        Node p2 = skolemize(p1);
        Node o2 = skolemize(o1);
        SPARQLQueryDefinition qdef = client.newQueryDefinition("");
        StringBuilder sb = new StringBuilder();
        sb.append("SELECT ?s ?p ?o where { ?s ?p ?o .");
        if (s2 != Node.ANY) {
            qdef.withBinding("a", s2.getURI());
            sb.append("FILTER (?s = ?a) ");
        }
        if (p2 != Node.ANY) {
            qdef.withBinding("b", p2.getURI());
            sb.append("FILTER (?p = ?b) ");
        }
        if (o2 != Node.ANY) {
            qdef = bindObject(qdef, "c", o2);
            sb.append("FILTER (?o = ?c) ");
        }
        sb.append("}");
        qdef.setSparql(sb.toString());
        qdef.setDefaultGraphUris(graphName);
        InputStreamHandle results = client.executeSelect(qdef,
                new InputStreamHandle());
        return results.get();
    }

    /**
     * @see org.apache.jena.sparql.core.DatasetGraphBaseFind
     */
    @Override
    protected Iterator<Quad> findInDftGraph(Node s, Node p, Node o) {
        checkIsOpen();
        sync();
        InputStream results = selectTriplesInGraph(DEFAULT_GRAPH_URI, s, p, o);
        return new QuadsIterator(results);
    }

    /**
     * @see org.apache.jena.sparql.core.DatasetGraphBaseFind
     */
    @Override
    protected Iterator<Quad> findInSpecificNamedGraph(Node g, Node s, Node p,
            Node o) {
        checkIsOpen();
        sync();
        InputStream results = selectTriplesInGraph(g.getURI(), s, p, o);
        return new QuadsIterator(g.getURI(), results);
    }

    /**
     * @see org.apache.jena.sparql.core.DatasetGraphBaseFind
     */
    @Override
    protected Iterator<Quad> findInAnyNamedGraphs(Node s, Node p, Node o) {
        checkIsOpen();
        sync();
        Node s1 = s != null ? s : Node.ANY;
        Node p1 = p != null ? p : Node.ANY;
        Node o1 = o != null ? o : Node.ANY;

        SPARQLQueryDefinition qdef = client.newQueryDefinition("");
        StringBuilder sb = new StringBuilder();
        sb.append("SELECT ?g ?s ?p ?o where {GRAPH ?g { ?s ?p ?o }");
        if (s1 != Node.ANY) {
            qdef.withBinding("a", s1.getURI());
            sb.append("FILTER (?s = ?a) ");
        }
        if (p1 != Node.ANY) {
            qdef.withBinding("b", p1.getURI());
            sb.append("FILTER (?p = ?b) ");
        }
        if (o1 != Node.ANY) {
            qdef = bindObject(qdef, "c", o1);
            sb.append("FILTER (?o = ?c) ");
        }
        sb.append("}");
        qdef.setSparql(sb.toString());
        InputStreamHandle results = client.executeSelect(qdef,
                new InputStreamHandle());
        return new QuadsIterator(results.get());
    }

    @Override
    /**
     * @see org.apache.jena.sparql.core.DatasetGraph
     */
    public void setDefaultGraph(Graph g) {
        this.addGraph(NodeFactory.createURI(DEFAULT_GRAPH_URI), g);
    }

    @Override
    /**
     * Start a transaction.
     */
    public void begin(ReadWrite readWrite) {
        checkIsOpen();
        sync();
        client.begin(readWrite);
    }

    @Override
    /**
     * Commit the current transaction.
     */
    public void commit() {
        checkIsOpen();
        sync();
        client.commit();
    }

    @Override
    /**
     * Abort the current transaction with a rollback operation.
     */
    public void abort() {
        checkIsOpen();
        sync();
        client.abort();
    }

    @Override
    /**
     * @return true if there is a multi-statement transaction in play.
     */
    public boolean isInTransaction() {
        checkIsOpen();
        return client.isInTransaction();
    }

    @Override
    /**
     * Synonymous with abort();
     */
    public void end() {
        abort();
    }

    /**
     * Gets a view of the DatasetGraph as a Dataset, which is used to back
     * queries.
     * @return The DatasetGraph, wrapped in a Dataset interface.
     */
    public Dataset toDataset() {
        checkIsOpen();
        return DatasetFactory.wrap(this);
    }

    /**
     * @see org.apache.jena.sparql.core.DatasetGraph
     */
    @Override
    public Graph getDefaultGraph() {
        checkIsOpen();
        sync();
        return GraphView.createDefaultGraph(this);
    }

    /**
     * @see org.apache.jena.sparql.core.DatasetGraph
     */
    @Override
    public Graph getGraph(Node graphNode) {
        checkIsOpen();
        sync();
        return GraphView.createNamedGraph(this, graphNode);
        //return client.readGraph(graphNode.getURI());
    }

    /**
     * @see org.apache.jena.sparql.core.DatasetGraph
     */
    @Override
    public void addGraph(Node graphName, Graph graph) {
        checkIsOpen();
        sync();
        client.writeGraph(graphName.getURI(), graph);
    }

    /**
     * Merges triples into a graph on the MarkLogic server. mergeGraph() is NOT
     * part of Jena's DatasetGraph interface.
     *
     * @param graphName
     *            The graph to merge with server state.
     * @param graph
     *            The graph data.
     */
    public void mergeGraph(Node graphName, Graph graph) {
        checkIsOpen();
        sync();
        client.mergeGraph(graphName.getURI(), graph);
    }

    /**
     * @see org.apache.jena.sparql.core.DatasetGraph
     */
    @Override
    public void removeGraph(Node graphName) {
        checkIsOpen();
        sync();
        client.deleteGraph(graphName.getURI());
    }

    @Override
    /**
     * Not supported by MarkLogicDatasetGraph.
     */
    public long size() {
        throw new UnsupportedOperationException("Dataset.size()");
    }

    /**
     * Gets the permissions associated with this graph.
     *
     * @param graphName
     *            the node with the graph's name.
     * @return A {@link com.marklogic.client.semantics.GraphPermissions} object
     *         holding the graph's permissions.
     */
    public GraphPermissions getPermissions(Node graphName) {
        checkIsOpen();
        return client.getGraphPermissions(graphName.getURI());
    }

    /**
     * Adds permissions to a graph.
     *
     * @param graphName
     *            the node with the graph's name.
     * @param permissions
     *            A {@link com.marklogic.client.semantics.GraphPermissions}
     *            object holding the graph's permissions.
     */
    public void addPermissions(Node graphName, GraphPermissions permissions) {
        checkIsOpen();
        client.mergeGraphPermissions(graphName.getURI(), permissions);
    }

    /**
     * Removes all but the default permissions from a graph.
     *
     * @param graphName
     *            the node with the graph's name.
     */
    public void clearPermissions(Node graphName) {
        checkIsOpen();
        client.deletePermissions(graphName.getURI());
    }

    /**
     * Sets the permissions on a graph.
     *
     * @param graphName
     *            the node with the graph's name.
     * @param permissions
     *            A {@link com.marklogic.client.semantics.GraphPermissions}
     *            object holding the graph's permissions.
     */
    public void writePermissions(Node graphName, GraphPermissions permissions) {
        checkIsOpen();
        client.writeGraphPermissions(graphName.getURI(), permissions);
    }

    /**
     * Forces the quads in the write cache to flush to the server.
     */
    public void sync() {
        client.syncAdds();
        client.syncDeletes();
    }

    /**
     * Used for deletes, forces the write buffer to sync before
     * constructing a delete buffer.
     */
    public void syncAdds() {
        client.syncAdds();
    }

    /**
     * Specifies a set of inferencing rulesets to apply to a query. These
     * rulesets either come with MarkLogic server or were installed by an
     * administrator.
     *
     * @param rulesets
     *            Zero-or-more rulesets to apply to queries.
     */
    public void setRulesets(SPARQLRuleset... rulesets) {
        this.rulesets = rulesets;
    }

    /**
     * Returns the array or rulesets currently used for SPARQL queries.
     *
     * @return An array of SPARQLRulesets.
     */
    public SPARQLRuleset[] getRulesets() {
        return this.rulesets;
    }

    /**
     * Fluent setter for rulesets.
     *
     * @param rulesets
     *            Zero-or-more rulesets to apply to queries.
     * @return The MarkLogicDatasetGraph, with rulesets set.
     */
    public MarkLogicDatasetGraph withRulesets(SPARQLRuleset... rulesets) {
        if (this.rulesets == null) {
            this.rulesets = rulesets;
        } else {
            Collection<SPARQLRuleset> collection = new ArrayList<SPARQLRuleset>();
            collection.addAll(Arrays.asList(this.rulesets));
            collection.addAll(Arrays.asList(rulesets));
            this.rulesets = collection.toArray(new SPARQLRuleset[] {});
        }
        return this;
    }

    /**
     * Sets a MarkLogic Java API QueryDefinition that is applied to SPARQL
     * queries to restrict documents upon which queries are run.
     *
     * @param constrainingQueryDefinition
     *            A query definition. Use raw query definitions or QueryBuilder.
     */
    public void setConstrainingQueryDefinition(
            QueryDefinition constrainingQueryDefinition) {
        this.constrainingQueryDefinition = constrainingQueryDefinition;
    }

    /**
     * Return the query defintion currently associated with SPARQL Queries
     * against this DatasetGraph.
     *
     * @return the QueryDefinition.
     */
    public QueryDefinition getConstrainingQueryDefinition() {
        return constrainingQueryDefinition;
    }

    /**
     * Return the com.marklogic.semantics.jena.client.JenaDatabaseClient
     *
     * @return THe active client associated with this graph.
     */
    public JenaDatabaseClient getDatabaseClient() {
        return this.client;
    }

    /**
     * Set the permissions for graphs created by this DatasetGraph during SPARQL
     * update operations. Set to null for default permissions.
     *
     * @param permissions
     *            One or more permissions to add to graphs created during SPARQL
     *            updates.
     */
    public void setSPARQLUpdatePermissions(GraphPermissions permissions) {
        this.updatePermissions = permissions;
    }

    /**
     * Get the permissions that are to be written to new graphs during SPARQL
     * update.
     *
     * @return the permissions associated with updates.
     */
    public GraphPermissions getSPARQLUpdatePermissions() {
        return this.updatePermissions;
    }

    /**
     * Closes the connection to the database. After closing the DatabaseGraph,
     * it can no longer be used for query or update.
     */
    @Override
    public void close() {
        checkIsOpen();
        this.client.close();
        this.client = null;
    }

    @Override
    public boolean supportsTransactions() {
        return true;
    }

    private void checkIsOpen() {
        if (client == null) {
            throw new MarkLogicJenaException("DatabaseGraph is closed");
        }
    }
}


/*
 * Copyright 2016-2018 MarkLogic Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.marklogic.semantics.jena.client;

import java.util.Iterator;
import java.util.Timer;

import org.apache.jena.graph.Graph;
import org.apache.jena.graph.Node;
import org.apache.jena.graph.Triple;
import org.apache.jena.query.ReadWrite;
import org.apache.jena.riot.Lang;
import org.apache.jena.riot.RDFDataMgr;
import org.apache.jena.riot.WriterGraphRIOT;
import org.apache.jena.sparql.graph.GraphFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.marklogic.client.DatabaseClient;
import com.marklogic.client.ResourceNotFoundException;
import com.marklogic.client.Transaction;
import com.marklogic.client.io.InputStreamHandle;
import com.marklogic.client.io.OutputStreamHandle;
import com.marklogic.client.semantics.GraphManager;
import com.marklogic.client.semantics.GraphPermissions;
import com.marklogic.client.semantics.RDFMimeTypes;
import com.marklogic.client.semantics.SPARQLQueryDefinition;
import com.marklogic.client.semantics.SPARQLQueryManager;
import com.marklogic.semantics.jena.MarkLogicDatasetGraph;
import com.marklogic.semantics.jena.MarkLogicTransactionException;

import static com.marklogic.semantics.jena.client.TripleBuffer.DEFAULT_CACHE_MILLIS;
import static com.marklogic.semantics.jena.client.TripleBuffer.DEFAULT_INITIAL_DELAY;

/**
 * A class to encapsulate access to the Java API's DatabaseClient for Jena
 * users. Access the underlying Java API client with getClient();
 * The DatabaseClient is a long-lived object.  You should expect to reuse
 * a single client for all connections that share state (username, password, etc.)
 */
public class JenaDatabaseClient {

    private GraphManager graphManager;
    private SPARQLQueryManager sparqlQueryManager;
    private TriplesWriteBuffer writeBuffer;
    private TriplesDeleteBuffer deleteBuffer;
    private DatabaseClient client;
    private Transaction currentTransaction;
    private Timer timer;
    private static Logger log = LoggerFactory
            .getLogger(JenaDatabaseClient.class);

    /**
     * Constructor.
     *
     * @param client
     *            a Java Client API DatabaseClient. Can be made with
     *            com.marklogic.client.DatabaseClientFactory
     */
    public JenaDatabaseClient(DatabaseClient client) {
        this(client, true);
    }

    public JenaDatabaseClient(DatabaseClient client, boolean periodicFlush) {
        this.client = client;
        this.graphManager = client.newGraphManager();
        this.graphManager.setDefaultMimetype(RDFMimeTypes.NTRIPLES);
        this.sparqlQueryManager = client.newSPARQLQueryManager();
        if (periodicFlush) {
            this.writeBuffer = new TriplesWriteBuffer(this);
            this.deleteBuffer = new TriplesDeleteBuffer(this);
            this.timer = new Timer();
            timer.schedule(writeBuffer, DEFAULT_INITIAL_DELAY, DEFAULT_CACHE_MILLIS);
            timer.schedule(deleteBuffer, DEFAULT_INITIAL_DELAY + 250, DEFAULT_CACHE_MILLIS);
        }
    }

    /**
     * Close the connection and free resources
     */
    public void close() {
        if (writeBuffer != null) {
            writeBuffer.cancel();
        }
        if (timer != null) {
            timer.cancel();
        }
        client = null;
    }

    /**
     * Set the interval at which the write and delete cache flush to MarkLogic.
     * Provided for internal diagnostic tuning.
     * @param millis The interval, in milliseconds at which the local write
     *               and delete buffers are to be flushed.
     */
    public void setTimerCacheInterval(long millis) {
        writeBuffer.setCacheInterval(millis);
        deleteBuffer.setCacheInterval(millis);
    }

    /**
     * Get the current interval for sending write and delete requests
     * to the server.
     * @return The current setting for writing and deleting triples, in milliseconds.
     */
    public long getTimerCacheInterval() {
        return writeBuffer.cacheMillis;
    }

    /**
     * Create a new {@link com.marklogic.client.semantics.SPARQLQueryDefinition}
     * from a query String. You can use the resulting object to configure
     * various aspects of the query or set binding variables.
     *
     * @param queryString
     *            A SPARQL Query or SPARQL Update.
     * @return A new
     *         {@link com.marklogic.client.semantics.SPARQLQueryDefinition}
     */
    public SPARQLQueryDefinition newQueryDefinition(String queryString) {
        return this.sparqlQueryManager.newQueryDefinition(queryString);
    }

    // synchronization needed because of setting of page length
    public synchronized void executeUpdate(SPARQLQueryDefinition qdef) {
        this.sparqlQueryManager.clearPageLength();
        this.sparqlQueryManager.executeUpdate(qdef, currentTransaction);
    }

    public boolean executeAsk(SPARQLQueryDefinition qdef) {
        return this.sparqlQueryManager.executeAsk(qdef, currentTransaction);
    }

    public InputStreamHandle executeConstruct(SPARQLQueryDefinition qdef,
            InputStreamHandle handle) {
        return this.sparqlQueryManager.executeConstruct(qdef, handle,
                currentTransaction);
    }

    public InputStreamHandle executeDescribe(SPARQLQueryDefinition qdef,
            InputStreamHandle handle) {
        return this.sparqlQueryManager.executeDescribe(qdef, handle,
                currentTransaction);
    }

    // synchronization needed because of setting of page length
    public synchronized InputStreamHandle executeSelect(SPARQLQueryDefinition qdef,
            InputStreamHandle handle, Long offset, Long limit) {
        if (limit == null) {
            this.sparqlQueryManager.clearPageLength();
        } else {
            this.sparqlQueryManager.setPageLength(limit);
        }
        if (offset != null) {
            return this.sparqlQueryManager.executeSelect(qdef, handle, offset,
                    currentTransaction);
        } else {
            return this.sparqlQueryManager.executeSelect(qdef, handle,
                    currentTransaction);
        }
    }

    public InputStreamHandle executeSelect(SPARQLQueryDefinition qdef,
            InputStreamHandle handle) {
        return executeSelect(qdef, handle, null, null);
    }

    public Iterator<String> listGraphUris() {
        return this.graphManager.listGraphUris();
    }

    public void mergeGraph(String uri, Graph graph) {
        WriterGraphRIOT writer = RDFDataMgr.createGraphWriter(Lang.NTRIPLES);
        OutputStreamRIOTSender sender = new OutputStreamRIOTSender(writer);
        sender.setGraph(graph);
        OutputStreamHandle handle = new OutputStreamHandle(sender);
        this.graphManager.merge(uri, handle, currentTransaction);
    }

    public void deleteGraph(String uri) {
        this.graphManager.delete(uri, currentTransaction);
    }

    public GraphPermissions getGraphPermissions(String uri) {
        return this.graphManager.getPermissions(uri, currentTransaction);
    }

    public void mergeGraphPermissions(String uri, GraphPermissions permissions) {
        this.graphManager
                .mergePermissions(uri, permissions, currentTransaction);
    }

    public void deletePermissions(String uri) {
        this.graphManager.deletePermissions(uri, currentTransaction);
    }

    public void writeGraphPermissions(String uri, GraphPermissions permissions) {
        this.graphManager
                .writePermissions(uri, permissions, currentTransaction);
    }

    public Transaction openTransaction() {
        return this.client.openTransaction();
    }

    public Graph readDefaultGraph() {
        return readGraph(MarkLogicDatasetGraph.DEFAULT_GRAPH_URI);
    }

    public Graph readGraph(String uri) {
        InputStreamHandle handle = new InputStreamHandle();
        Graph graph = GraphFactory.createDefaultGraph();
        try {
            this.graphManager.read(uri, handle, currentTransaction);
            RDFDataMgr.read(graph, handle.get(), Lang.NTRIPLES);
        } catch (NullPointerException e) {
            log.debug("RDF Manager Throws execption for empty results");
        } catch (ResourceNotFoundException e) {
            log.debug("No graph returned by graph manage.  Returning empty graph", e.getMessage());
        }
        // close handle?
        return graph;
    }

    public void writeGraph(String uri, Graph graph) {
        WriterGraphRIOT writer = RDFDataMgr.createGraphWriter(Lang.NTRIPLES);
        OutputStreamRIOTSender sender = new OutputStreamRIOTSender(writer);
        sender.setGraph(graph);
        OutputStreamHandle handle = new OutputStreamHandle(sender);
        this.graphManager.write(uri, handle, currentTransaction);
    }
    /**
     * If timer is turned on (periodicFlush = true in constructor)
     * this method puts a quad into the cache which is periodically sent to MarkLogic
     * otherwise it sends the quad directly
     *
     * @param g
     *            Graph node.
     * @param s
     *            Subject node
     * @param p
     *            Property node.
     * @param o
     *            Object Node.
     */
    public void sinkQuad(Node g, Node s, Node p, Node o) {
        if (writeBuffer != null) {
            writeBuffer.add(g, s, p, o);
        } else {
            Graph graph = GraphFactory.createDefaultGraph();
            graph.add(Triple.create(s,p,o));
            mergeGraph(g.getURI(), graph);
            return;
        }
    }

    /**
     * Flushes the write cache, ensuring consistent server state before
     * query/delete.
     */
    public void syncAdds() {
        if (writeBuffer != null) {
            writeBuffer.forceRun();
        }
    }

    /**
     * Flushes the quads accumulated in the delete buffer
     */
    public void syncDeletes() {
        if (deleteBuffer != null) {
            deleteBuffer.forceRun();
        }
    }

    public void sinkDelete(Node g, Node s, Node p, Node o) {
        if (deleteBuffer != null) {
            deleteBuffer.add(g, s, p, o);
        } else {
            // FIXME  no delete buffer.
        }
    }

    private void checkCurrentTransaction() {
        if (this.currentTransaction == null) {
            throw new MarkLogicTransactionException("No open transaction");
        }
    }

    public void begin(ReadWrite readWrite) {
        if (readWrite == ReadWrite.READ) {
            throw new MarkLogicTransactionException(
                    "MarkLogic only supports write transactions");
        } else {
            if (this.currentTransaction != null) {
                throw new MarkLogicTransactionException(
                        "Only one open transaction per MarkLogicDatasetGraph instance.");
            }
            this.currentTransaction = openTransaction();
        }
    }

    public void commit() {
        checkCurrentTransaction();
        this.currentTransaction.commit();
        this.currentTransaction = null;
    }

    public void abort() {
        try {
          checkCurrentTransaction();
          currentTransaction.rollback();
        } finally {
          currentTransaction = null;
        }
    }

    public boolean isInTransaction() {
        return (this.currentTransaction != null);
    }

}


/*
 * Copyright 2016-2018 MarkLogic Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.marklogic.semantics.jena;

import com.marklogic.client.DatabaseClient;
import com.marklogic.client.DatabaseClientFactory;
import com.marklogic.client.DatabaseClientFactory.Authentication;
import com.marklogic.semantics.jena.client.JenaDatabaseClient;
import com.marklogic.semantics.jena.engine.MarkLogicQueryEngine;
import com.marklogic.semantics.jena.engine.MarkLogicUpdateEngine;

/**
 * Contains static methods for creating a MarkLogicDatasetGraph, which is a
 * DatabsetGraph that is backed by a MarkLogic database and REST API.
 */
public class MarkLogicDatasetGraphFactory {

    /**
     * Creates a MarkLogicDatasetGraph from an existing
     * {@link com.marklogic.client.DatabaseClient}.
     *
     * @param client
     *            An instance of DatabaseClient.
     * @return A MarkLogicDatasetGraph instance wrapping MarkLogic.
     */
    public static MarkLogicDatasetGraph createDatasetGraph(DatabaseClient client) {
        JenaDatabaseClient jenaClient = new JenaDatabaseClient(client);
        MarkLogicDatasetGraph datasetGraph = new MarkLogicDatasetGraph(
                jenaClient);
        MarkLogicQueryEngine.unregister();
        MarkLogicQueryEngine.register();
        MarkLogicUpdateEngine.unregister();
        MarkLogicUpdateEngine.register();
        return datasetGraph;
    }

    /**
     * Creates MarkLogicDatasetGraph from access parameters to a REST MarkLogic
     * server.
     *
     * @param host
     *            the host with the REST server
     * @param port
     *            the port for the REST server
     * @param user
     *            the user with read, write, or administrative privileges
     * @param password
     *            the password for the user
     * @param type
     *            the type of authentication applied to the request
     * @return A MarkLogicDatasetGraph instance wrapping MarkLogic.
     */
    static public MarkLogicDatasetGraph createDatasetGraph(String host,
            int port, String user, String password, Authentication type) {
        DatabaseClient client = DatabaseClientFactory.newClient(host, port,
                user, password, type);
        return MarkLogicDatasetGraphFactory.createDatasetGraph(client);
    }
}


/*
 * Copyright (c) 2011 Original Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.gm4java.engine.support;

import org.gm4java.engine.GMConnection;
import org.gm4java.engine.GMException;
import org.gm4java.engine.GMService;
import org.gm4java.engine.GMServiceException;

import java.io.IOException;
import java.util.List;

import javax.annotation.Nonnull;

/**
 * A {@link GMService} that manages and uses a pool of GraphicsMagick instances. This implementation uses
 * {@link org.apache.commons.pool.impl.GenericObjectPool} from Apache Commons
 * Pool project.
 *
 * @author Kenneth Xu
 *
 */
public class PooledGMService implements GMService {
    private final GMConnectionPool pool;

    /**
     * Construct a new instance of {@linkplain PooledGMService} with given pool configuration.
     *
     * @param config
     *            configuration of the GraphicsMagick process pool
     */
    public PooledGMService(@Nonnull GMConnectionPoolConfig config) {
        pool = new GMConnectionPool(config);
    }

    PooledGMService(GMConnectionPool pool) {
        this.pool = pool;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String execute(@Nonnull String command, String... arguments) throws IOException, GMException,
            GMServiceException {
        PooledGMConnection connection = pool.borrowObject();
        try {
            return connection.execute(command, arguments);
        } finally {
            pool.returnObject(connection);
        }
    }

    @Override
    public String execute(List<String> command) throws IOException, GMException, GMServiceException {
        PooledGMConnection connection = pool.borrowObject();
        try {
            return connection.execute(command);
        } finally {
            pool.returnObject(connection);
        }
    }

    /**
     * {@inheritDoc}
     *

     * This implementation returns an instance from pool.
     */
    @Override
    @Nonnull
    public GMConnection getConnection() throws GMServiceException {
        return new ConnectionWrapper(pool);
    }

    void setProcessFactoryBuilder(GMProcessFactory.Builder builder) {
        pool.setProcessFactoryBuilder(builder);
    }

    private static final class ConnectionWrapper implements GMConnection {
        @Nonnull
        private final GMConnectionPool pool;
        private PooledGMConnection real;

        private ConnectionWrapper(GMConnectionPool pool) throws GMServiceException {
            this.pool = pool;
            real = pool.borrowObject();
        }

        @Override
        public String execute(@Nonnull String command, String... arguments) throws IOException, GMException,
                GMServiceException {
            assertConnectionNotClosed();
            return real.execute(command, arguments);
        }

        @Override
        public String execute(List<String> command) throws IOException, GMException, GMServiceException {
            assertConnectionNotClosed();
            return real.execute(command);
        }

        @Override
        public void close() throws GMServiceException {
            if (real == null) return;
            pool.returnObject(real);
            real = null;
        }

        private void assertConnectionNotClosed() throws GMServiceException {
            if (real == null) throw new GMServiceException("GMConnection is already closed.");
        }
    }
}


package org.gm4java.engine.support;

import org.apache.commons.pool.BasePoolableObjectFactory;
import org.apache.commons.pool.impl.GenericObjectPool;
import org.gm4java.engine.GMServiceException;

import java.io.IOException;

import javax.annotation.Nonnull;

/**
 * Manages a pool of GraphicsMagick instances represented by {@link PooledGMConnection}. The implementation builds upon
 * {@link GenericObjectPool} from Apache Commons Pool project.
 *
 * @author Kenneth Xu
 *
 */
class GMConnectionPool extends GenericObjectPool<PooledGMConnection> {
    private GMProcessFactory.Builder builder;
    private GMProcessFactory gmFactory;
    private int evictAfterNumberOfUse = GMConnectionPoolConfig.DEFAULT_EVICT_AFTER_NUMBER_OF_USE;

    /**
     * Construct a new instance of {@linkplain GMConnectionPool}.
     */
    GMConnectionPool(@Nonnull GMConnectionPoolConfig config) {
        this(new Factory(), config);
    }

    private GMConnectionPool(Factory factory, GMConnectionPoolConfig config) {
        super(factory, getParentConfig(config));
        factory.pool = this;
        evictAfterNumberOfUse = config.getEvictAfterNumberOfUse();

        this.builder = GMProcessFactoryImpl.BUILDER;
        gmFactory = builder.buildFactory(config.getGMPath());
    }

    private static GenericObjectPool.Config getParentConfig(GMConnectionPoolConfig config) {
        if (config == null) throw new NullPointerException("config");
        return config.getConfig();
    }

    /**
     * {@inheritDoc}
     *

     * This implementation delegates to super class but ensures that there is no other checked exception except
     * {@link GMServiceException} will be thrown.
     */
    @Override
    @Nonnull
    public PooledGMConnection borrowObject() throws GMServiceException {
        try {
            return super.borrowObject();
        } catch (GMServiceException e) {
            throw e;
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new GMServiceException(e.getMessage(), e);
        }
    }

    /**
     * {@inheritDoc}
     *

     * This implementation delegates to super class but ensures that there is no other checked exception except
     * {@link GMServiceException} will be thrown.
     *
     */
    @Override
    public void returnObject(@Nonnull PooledGMConnection connection) throws GMServiceException {
        try {
            super.returnObject(connection);
        } catch (GMServiceException e) {
            throw e;
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new GMServiceException(e.getMessage(), e);
        }
    };

    /**
     * Gets the path to GraphicsMagick executable set by {@link #setGMPath(String)} or
     * {@link GMConnectionPoolConfig#DEFAULT_GM_PATH} if it was not explicitly set.
     *
     * @return the path to GraphicsMagick executable
     */
    public String getGMPath() {
        return gmFactory.getGMPath();
    }

    /**
     * Sets the path to GraphicsMagick executable.
     *
     * @param gmPath
     *            the path to GraphicsMagick executable
     */
    public void setGMPath(@Nonnull String gmPath) {
        if (gmPath == null) throw new NullPointerException("gmPath");
        gmFactory = builder.buildFactory(gmPath);
    }

    /**
     * Gets the values set by {@link #setEvictAfterNumberOfUse(int)}.
     *
     * @return the values set by {@link #setEvictAfterNumberOfUse(int)}
     */
    public int getEvictAfterNumberOfUse() {
        return evictAfterNumberOfUse;
    }

    /**
     * Sets the number of times a GraphicsMagick process can be used to execute commands before it gets evicted and
     * destroyed. A non-positive value disables this feature. This feature is disabled by default until a positive value
     * is set.
     *

     * Note: This setting doesn't guarantee the max number of times a GraphicsMagick process is used to execute
     * the command. The eviction and destruction can only occur when the {@link PooledGMConnection} instance is borrowed
     * from or returned back to the pool. But a client can get hold of the connection and execute as many commands as it
     * wants.
     *
     * @param evictAfterNumberOfUse
     *            the number of times a GraphicsMagick process can execute commands before the pool mark it for eviction
     *            and destroy
     */
    public void setEvictAfterNumberOfUse(int evictAfterNumberOfUse) {
        this.evictAfterNumberOfUse = evictAfterNumberOfUse;
    }

    void setProcessFactoryBuilder(GMProcessFactory.Builder builder) {
        this.builder = builder;
        gmFactory = builder.buildFactory(gmFactory.getGMPath());
    }

    ReaderWriterProcess createProcess() throws GMServiceException {
        try {
            return gmFactory.getProcess();
        } catch (IOException e) {
            throw new GMServiceException(e.getMessage(), e);
        }
    }

    private static final class Factory extends BasePoolableObjectFactory<PooledGMConnection> {

        private GMConnectionPool pool;

        @Override
        public PooledGMConnection makeObject() throws GMServiceException {
            return new PooledGMConnection(pool);
        }

        @Override
        public void destroyObject(PooledGMConnection connection) throws GMServiceException {
            connection.close();
        }

        @Override
        public boolean validateObject(PooledGMConnection connection) {
            try {
                connection.execute("ping");
                return true;
            } catch (Exception e) {
                return false;
            }
        }

        @Override
        public void activateObject(PooledGMConnection connection) throws GMServiceException {
            connection.ensureHealthy();
        }

        @Override
        public void passivateObject(PooledGMConnection connection) throws GMServiceException {
            connection.ensureHealthy();
        }
    }
}


/*
 * Copyright (c) 2011 Original Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.gm4java.im4java;

import org.gm4java.engine.GMException;
import org.gm4java.engine.GMExecutor;
import org.im4java.core.ImageCommand;
import org.im4java.process.ErrorConsumer;
import org.im4java.process.OutputConsumer;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.LinkedList;

import javax.annotation.Nonnull;

/**
 *
 * This class wraps the GM command in interactive or batch mode.
 * <p>
 * Note: Does not support asynchronous mode; Does not support BufferedImage as output.
 *
 * @author Kenneth Xu
 *
 */
public class GMBatchCommand extends ImageCommand {
    private final GMExecutor executor;
    private OutputConsumer outputConsumer;
    private ErrorConsumer errorConsumer;

    /**
     * Construct a new instance of {@link GMBatchCommand} that uses given executor to execute specified command.
     *
     * @param executor
     *            executor to execute the command
     * @param command
     *            command to be executed.
     */
    public GMBatchCommand(@Nonnull GMExecutor executor, @Nonnull String command) {
        super(command);
        this.executor = executor;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setOutputConsumer(@Nonnull OutputConsumer pOutputConsumer) {
        super.setOutputConsumer(pOutputConsumer);
        outputConsumer = pOutputConsumer;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setErrorConsumer(@Nonnull ErrorConsumer pErrorConsumer) {
        super.setErrorConsumer(pErrorConsumer);
        this.errorConsumer = pErrorConsumer;
    }

    /**
     * {@inheritDoc}
     *
     * This implementation uses {@link GMExecutor} to execute the command.
     */
    @Override
    protected int run(@Nonnull LinkedList<String> pArgs) throws Exception {

        int rc;
        try {
            String result = executor.execute(pArgs);
            if (outputConsumer != null && result != null) outputConsumer.consumeOutput(stringToStream(result));
            rc = 0;
        } catch (GMException e) {
            if (errorConsumer != null) errorConsumer.consumeError(stringToStream(e.getMessage()));
            else throw e;
            rc = 1;
        }
        finished(rc);
        return rc;
    }

    /**
     * Unlike other implementation of {@link ImageCommand}, {@link GMBatchCommand} uses {@link GMExecutor} instead of
     * starting a new GraphicsMagick process. This makes {@link #setSearchPath(String)} method useless. Calling this
     * method always result in {@link UnsupportedOperationException}.
     *
     * @param pSearchPath
     *            not used
     */
    @Override
    @Deprecated
    public void setSearchPath(String pSearchPath) {
        throw new UnsupportedOperationException("GMBatchCommand does not support setSearchPath because it uses the"
                + " GMExecutor passed to the constructor instead of starting a GraphicsMagick process itself.");
    }

    @Nonnull
    private InputStream stringToStream(@Nonnull String s) {
        return new ByteArrayInputStream(s.getBytes());
    }
}


/*
 * Copyright (c) 2011 Original Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.gm4java.engine.support;

import org.gm4java.engine.GMConnection;
import org.gm4java.engine.GMException;
import org.gm4java.engine.GMService;
import org.gm4java.engine.GMServiceException;

import java.io.IOException;
import java.util.List;

import javax.annotation.Nonnull;

/**
 * Implementation of {@link GMService} that creates a new GraphicsMagick process every time {@link #getConnection()} is
 * called.
 *
 * @author Kenneth Xu
 *
 */
public class SimpleGMService implements GMService {
    /**
     * Default path to GraphcisMagick executable is simply "gm".
     */
    public static final String DEFAULT_GM_PATH = Constants.DEFAULT_GM_PATH;

    private final GMProcessFactory.Builder builder;
    private GMProcessFactory factory;

    /**
     * Create a new instance of SimpleGMService.
     */
    public SimpleGMService() {
        this(GMProcessFactoryImpl.BUILDER);
    }

    SimpleGMService(GMProcessFactory.Builder builder) {
        this.builder = builder;
        factory = builder.buildFactory(DEFAULT_GM_PATH);
    }

    /**
     * Gets the path to GraphicsMagick executable set by {@link #setGMPath(String)} or {@link #DEFAULT_GM_PATH} if it
     * was not explicitly set.
     *
     * @return the path to GraphicsMagick executable
     */
    @Nonnull
    public String getGMPath() {
        return factory.getGMPath();
    }

    /**
     * Sets the path to GraphicsMagick executable.
     *
     * @param gmPath
     *            the path to GraphicsMagick executable
     */
    public void setGMPath(@Nonnull String gmPath) {
        if (gmPath == null) throw new NullPointerException("gmPath");
        factory = builder.buildFactory(gmPath);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String execute(@Nonnull String command, String... arguments) throws GMException, GMServiceException,
            IOException {
        final GMConnection connection = getConnection();
        try {
            return connection.execute(command, arguments);
        } finally {
            connection.close();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String execute(List<String> command) throws GMException, GMServiceException, IOException {
        final GMConnection connection = getConnection();
        try {
            return connection.execute(command);
        } finally {
            connection.close();
        }
    }

    /**
     * {@inheritDoc}
     *
     * This implementation creates a new instance.
     */
    @Override
    @Nonnull
    public GMConnection getConnection() throws GMServiceException {
        try {
            return new BasicGMConnection(factory.getProcess());
        } catch (IOException e) {
            throw new GMServiceException(e.getMessage(), e);
        }
    }
}


/*
 * Copyright (c) 2011 Original Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.gm4java.im4java;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

import javax.annotation.CheckForNull;

/**
 *
 * This class extends the im4java version of GMOperation and provide a list of convenience methods to ease the
 * construction of the image command for GraphicsMagick.
 *
 * @author Michael Magpayo
 *
 */
public class GMOperation extends org.im4java.core.GMOperation {

    /** Options which modify the behavior of the 'degrees' specification for rotation-based settings. */
    public enum RotationAnnotation {
        /** Forces rotation. */
        Always(""),
        /** Rotate the image if and only if its width exceeds the height. */
        WidthExceedsHeightOnly(">"),
        /** Rotate the image if and only if its height exceeds the width. */
        HeightExceedsWidthOnly("<");

        private final String annotation;

        private RotationAnnotation(final String annotation) {
            this.annotation = annotation;
        }

        /**
         * Serializes option for GraphicsMagick command-line.
         *
         * @return Annotation to append to geometry specification
         */
        public String asAnnotation() {
            return annotation;
        }
    }

    /**
     * Used in conjunction with other settings, generally defines how to interpret the offset for geometry-based
     * options.
     */
    public enum Gravity {
        /** Offset defined from top-left corner. */
        NorthWest,
        /** Offset defined from top edge at the midpoint. */
        North,
        /** Offset defined from top-right corner. */
        NorthEast,
        /** Offset defined from left edge at the midpoint. */
        West,
        /** Offset defined from image center point. */
        Center,
        /** Offset defined from right edge at the midpoint. */
        East,
        /** Offset defined from bottom-left corner. */
        SouthWest,
        /** Offset defined from bottom edge at the midpoint. */
        South,
        /** Offset defined from bottom-right corner. */
        SouthEast;
    }

    /** Options which modify the behavior of the 'width X height' specification for geometry-based settings. */
    public enum GeometryAnnotation {
        /** Resize the image while maintaining the aspect ratio using the geometry specification as minimum values. */
        FillUsingAspectRatio("^"),
        /** Resize the image while maintaining the aspect ratio using the geometry specification as maximum values. */
        FitUsingAspectRatio(""),
        /** Resize of the image to exactly match the geometry specification. */
        ForceDimensions("!"),
        /** Treat width and height declarations as percentage values instead of pixel lengths. */
        DimensionsAsPercentages("%"),
        /** Resize the image only if its width or height exceeds the geometry specification. */
        DownsizeOnly(">"),
        /** Resize the image only if both width and height are less than the geometry specification. */
        UpsizeOnly("<");

        private final String annotation;

        private GeometryAnnotation(final String annotation) {
            this.annotation = annotation;
        }

        /**
         * Serializes option for GraphicsMagick command-line.
         *
         * @return Annotation to append to geometry specification
         */
        public String asAnnotation() {
            return annotation;
        }

        /**
         * Ensure that the {@code used} annotations are a valid set of annotations.
         *
         * @param used
         *            Annotation set to scan.
         */
        static void validate(final Collection<GeometryAnnotation> used) {
            // '<' and '>' annotations
            throwMutuallyExclusiveException(used, false,
                    EnumSet.of(GeometryAnnotation.DownsizeOnly, GeometryAnnotation.UpsizeOnly));

            // '', '^' and '!' annotations
            throwMutuallyExclusiveException(used, true, EnumSet.of(GeometryAnnotation.FillUsingAspectRatio,
                    GeometryAnnotation.FitUsingAspectRatio, GeometryAnnotation.ForceDimensions));
        }

        /**
         * Throws {@link IllegalArgumentException} if more than one option from {@code mutuallyExclusive} annotations
         * exist in the {@code used} set.
         */
        private static void throwMutuallyExclusiveException(final Collection<GeometryAnnotation> used,
                final boolean shouldHaveAtLeastOne, final Collection<GeometryAnnotation> mutuallyExclusive) {
            final EnumSet<GeometryAnnotation> matched = EnumSet.copyOf(used);
            matched.retainAll(mutuallyExclusive);
            if (matched.size() > 1) {
                throw new IllegalArgumentException("Geometry annotations " + join(matched, "and")
                        + "are mutually exclusive");
            }
            if (shouldHaveAtLeastOne && (matched.size() != 1)) {
                throw new IllegalArgumentException("One of the following geometry annotations must be used: "
                        + join(mutuallyExclusive, "or"));
            }
        }

        private static String join(Collection<?> set, String andOr) {
            StringBuffer buf = new StringBuffer();
            Iterator<?> i = set.iterator();
            for (int n = set.size() - 2; n > 0; n--) {
                buf.append(i.next()).append(", ");
            }
            buf.append(i.next()).append(' ').append(andOr).append(' ').append(i.next());
            return buf.toString();
        }
    }

    /**
     * Limits the number of threads used by the GraphicsMagick process during execution. Note that no validation is
     * made, so ensure that the value is a non-positive integer presumably less than the maximum CPU cores on the host.
     *
     * @param threadsPerProcess
     *            Number of threads to use per process.
     * @return Builder object for chained options setup.
     */
    public GMOperation limitThreads(final int threadsPerProcess) {
        final List<String> args = getCmdArgs();
        args.add("-limit");
        args.add("threads");
        args.add(Integer.toString(threadsPerProcess));
        return this;
    }

    /**
     * Resize source to desired target dimensions, using default resizing filter algorithm.
     *
     * @param width
     *            Length in pixels of the target image's width.
     * @param height
     *            Length in pixels of the target image's height.
     * @param annotations
     *            Geometry annotations to define how the {@code width} and {@code height} options are to be interpreted.
     * @return Builder object for chained options setup.
     */
    public GMOperation resize(final int width, final int height, final Collection<GeometryAnnotation> annotations) {
        final List<String> args = getCmdArgs();
        args.add("-resize");
        args.add(resample(width, height, annotations));
        return this;
    }

    /**
     * Scale source to desired target dimensions, using simplified resizing algorithm with pixel averaging.
     *
     * @param width
     *            Length in pixels of the target image's width.
     * @param height
     *            Length in pixels of the target image's height.
     * @param annotations
     *            Geometry annotations to define how the {@code width} and {@code height} options are to be interpreted.
     * @return Builder object for chained options setup.
     */
    public GMOperation scale(final int width, final int height, final Collection<GeometryAnnotation> annotations) {
        final List<String> args = getCmdArgs();
        args.add("-scale");
        args.add(resample(width, height, annotations));
        return this;
    }

    /**
     * Rotates the image, with empty triangles back-filled using default background color.
     *
     * @param degrees
     *            The angle by which the image should be rotated. Positive numbers indicate clockwise direction;
     *            negative numbers indicate counterclockwise direction.
     * @param annotation
     *            Rotate annotation to define when rotation should happen.
     * @return Builder object for chained options setup.
     */
    public GMOperation rotate(final double degrees, final RotationAnnotation annotation) {
        if (annotation == null) {
            throw new IllegalArgumentException("Rotation annotation must be defined");
        }
        final List<String> args = getCmdArgs();
        args.add("-rotate");
        args.add(String.format(Locale.ENGLISH,"%.1f%s", degrees, annotation.asAnnotation()));
        return this;
    }

    /**
     * Defines the gravity for geometry-based operations. See documentation for more details, as this option works in
     * conjunction with various options in different ways.
     *
     * @param value
     *            Gravity value to use, defining the coordinate system to use.
     * @return Builder object for chained options setup.
     */
    public GMOperation gravity(final Gravity value) {
        if (value == null) {
            throw new IllegalArgumentException("Gravity value must be defined");
        }
        gravity(value.toString());
        return this;
    }

    /**
     * Convert the image to grayscale (i.e. replace color encoding with smaller gray encoding).
     *
     * @param depth
     *            the depth of the grayscale
     * @return Builder object for chained options setup.
     */
    public GMOperation grayscale(final int depth) {
        type("Grayscale");
        depth(depth);
        return this;
    }

    /**
     * Strips out ICC profiles.
     *
     * @return Builder object for chained options setup.
     */
    public GMOperation stripProfiles() {
        final List<String> args = getCmdArgs();
        args.add("+profile");
        args.add("*");
        return this;
    }

    /**
     * Defines font for text overlay.
     *
     * @param style
     *            Text font style (system-dependent based on installed fonts).
     * @param size
     *            Text point size.
     * @param color
     *            Text color, either named or {@code #RGB} variant.
     * @return Builder object for chained options setup.
     */
    public GMOperation font(final String style, final int size, final String color) {
        if (isBlank(style)) {
            throw new IllegalArgumentException("Text font style must be defined");
        }
        if (isBlank(color)) {
            throw new IllegalArgumentException("Text font color must be defined");
        }
        font(style);
        pointsize(size);
        fill(color);
        return this;
    }

    /**
     * Draws text overlay on image with the upper-right corner defined by the {@code offsetX} and {@code offsetY}
     * parameters. Note that {@link#gravity(Gravity)} will affect how the offset values are interpreted.
     *
     * @param text
     *            Text to write.
     * @param offsetX
     *            Offset value in the horizontal direction.
     * @param offsetY
     *            Offset value in the vertical direction.
     * @return Builder object for chained options setup.
     */
    public GMOperation drawText(final String text, final int offsetX, final int offsetY) {
        if (isBlank(text)) {
            throw new IllegalArgumentException("Text string must be defined");
        }
        draw(String.format(Locale.ENGLISH,"text %d %d '%s'", offsetX, offsetY, text));
        return this;
    }

    /**
     * Specifies the source image to convert.
     *
     * @param file
     *            Source image file to convert.
     * @param width
     *            Length in pixels of the source image's width; if {@code null} or negative, the in-memory size
     *            definition is omitted. Optional.
     * @param height
     *            Length in pixels of the source image's height; if {@code null} or negative, the in-memory size
     *            definition is omitted. Optional.
     * @return Builder object for chained options setup.
     * @throws IOException
     *             when the source file does not exist.
     */
    public GMOperation source(final File file, @CheckForNull final Integer width, @CheckForNull final Integer height)
            throws IOException {
        if (file != null && !file.exists()) {
            throw new IOException("Source file '" + file + "' does not exist");
        }
        if ((width != null) && (height != null) && (width > 0) && (height > 0)) {
            size(width, height);
        }
        return addImage(file);
    }

    /**
     * Add image to operation.
     *
     * @param file
     *            image file to be added.
     * @return Builder object for chained options setup.
     */
    public GMOperation addImage(final File file) {
        if (file == null) {
            throw new IllegalArgumentException("file must be defined");
        }
        getCmdArgs().add(file.getPath());
        return this;
    }

    /**
     * Add raw text to the list of arguments.
     *
     * @param arg
     *            argument to add
     * @return Builder object for chained options setup.
     */
    public GMOperation addRawArg(String arg) {
        getCmdArgs().add(arg);
        return this;
    }

    /**
     * Add option -quality to the GraphicsMagick commandline (see the documentation of GraphicsMagick for details).
     *
     * @param quality
     *            the quality of the image from 0 - 100
     * @return Builder object for chained options setup
     */
    public GMOperation quality(double quality) {
        final List<String> args = getCmdArgs();
        args.add("-quality");
        args.add(Double.toString(quality));
        return this;
    }

    private static String resample(final int width, final int height, final Collection<GeometryAnnotation> annotations) {
        if ((width < 1) || (height < 1)) {
            throw new IllegalArgumentException("Target height and width both should be greater than zero");
        }
        if (annotations == null) {
            throw new IllegalArgumentException("Geometry annotation(s) must be defined");
        }
        GeometryAnnotation.validate(annotations);
        StringBuilder buf = new StringBuilder();
        buf.append(width).append('x').append(height);
        for (final GeometryAnnotation entry : annotations) {
            buf.append(entry.asAnnotation());
        }
        return buf.toString();
    }

    private static boolean isBlank(String s) {
        return (s == null) || s.length() == 0;
    }

}


package io.vertx.ext.prometheus.metrics.factories;

import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.Histogram;
import org.jetbrains.annotations.NotNull;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * A factory for shared histograms.
 *
 * @author jansorg
 */
public final class HistogramFactory {
  private final @NotNull CollectorRegistry registry;
  private final @NotNull Map<String, Histogram> histograms = new ConcurrentHashMap<>();

  public HistogramFactory(@NotNull CollectorRegistry registry) {
    this.registry = registry;
  }

  /**
   * Unregisters the histograms created by this factory from the registry.
   */
  public void close() {
    histograms.values().forEach(registry::unregister);
  }

  /**
   * Time histogram.
   *
   * @param name The name of the histogram, without prefix and suffix.
   * @return A histogram for http requests, identified by the given name. Histograms with the same name are shared.
   */
  public @NotNull Histogram timeSeconds(@NotNull String name) {
    return histograms.computeIfAbsent("vertx_" + name + "_time_seconds", key -> register(Histogram.build(key, "Processing time in seconds")
        .labelNames("local_address")
        .create()));
  }

  private @NotNull Histogram register(@NotNull Histogram histogram) {
    registry.register(histogram);
    return histogram;
  }
}


package io.vertx.ext.prometheus.metrics.factories;

import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.Gauge;
import org.jetbrains.annotations.NotNull;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * A factory for shared gauges.
 * Gauges are identified by name, a gauge can only be shared once in a {@link CollectorRegistry}.
 *
 * @author jansorg
 */
public final class GaugeFactory {
  private final @NotNull CollectorRegistry registry;
  private final @NotNull Map<String, Gauge> gauges = new ConcurrentHashMap<>();

  public GaugeFactory(@NotNull CollectorRegistry registry) {
    this.registry = registry;
  }

  /**
   * Unregisters the gauges created by this factory from the registry.
   */
  public void close() {
    gauges.values().forEach(registry::unregister);
  }

  /**
   * HTTP requests gauge.
   *
   * @param name The name of the gauge, without prefix and suffix.
   * @return A gauge for http requests, identified by the given name. Gauges with the same name are shared.
   */
  public @NotNull Gauge httpRequests(@NotNull String name) {
    return gauges.computeIfAbsent("vertx_" + name + "_requests", key -> register(Gauge.build(key, "HTTP requests number")
        .labelNames("local_address", "method", "path", "state")
        .create()));
  }

  /**
   * Websockets requests gauge.
   *
   * @param name The name of the gauge, without prefix and suffix.
   * @return A gauge for websockets, identified by the given name. Gauges with the same name are shared.
   */
  public @NotNull Gauge websockets(@NotNull String name) {
    return gauges.computeIfAbsent("vertx_" + name + "_websockets", key -> register(Gauge.build(key, "Websockets number")
        .labelNames("local_address").create()));
  }

  /**
   * Connections gauge.
   *
   * @param name The name of the gauge, without prefix and suffix.
   * @return A gauge for connections, identified by the given name. Gauges with the same name are shared.
   */
  public @NotNull Gauge connections(@NotNull String name) {
    return gauges.computeIfAbsent("vertx_" + name + "_connections", key -> register(Gauge.build(key, "Active connections number")
        .labelNames("local_address").create()));
  }

  /**
   * Endpoints gauge.
   *
   * @param name The name of the gauge, without prefix and suffix.
   * @return A gauge for endpoints, identified by the given name. Gauges with the same name are shared.
   */
  public @NotNull Gauge endpoints(@NotNull String name) {
    return gauges.computeIfAbsent("vertx_" + name + "_endpoints", key -> register(Gauge.build(key, "Endpoints number")
        .labelNames("local_address", "state").create()));
  }

  private @NotNull Gauge register(@NotNull Gauge gauge) {
    registry.register(gauge);
    return gauge;
  }
}


package io.vertx.ext.prometheus.metrics.factories;

import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.Counter;
import org.jetbrains.annotations.NotNull;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Factory for shared counters.
 * Counters are identified by name and can only be registered once with a name.
 *
 * @author jansorg
 */
public final class CounterFactory {
  private final @NotNull CollectorRegistry registry;
  private final @NotNull Map<String, Counter> counters = new ConcurrentHashMap<>();

  public CounterFactory(@NotNull CollectorRegistry registry) {
    this.registry = registry;
  }

  /**
   * Unregisters the counters which were created by this factory from the registry.
   */
  public void close() {
    counters.values().forEach(registry::unregister);
  }

  /**
   * Bytes counter.
   *
   * @param name The name of the counter, without prefix and suffix.
   * @return A counter of bytes for the given name. Counters with the same name are shared.
   */
  public @NotNull Counter bytes(@NotNull String name) {
    return counters.computeIfAbsent("vertx_" + name + "_bytes", key -> register(Counter.build(key, "Read/written bytes")
        .labelNames("local_address", "type").create()));
  }

  /**
   * Errors counter.
   *
   * @param name The name of the counter, without prefix and suffix.
   * @return A counter for errors, identified by the given name. Counters with the same name are shared.
   */
  public @NotNull Counter errors(@NotNull String name) {
    return counters.computeIfAbsent("vertx_" + name + "_errors", key -> register(Counter.build(key, "Errors number")
        .labelNames("local_address", "class").create()));
  }

  /**
   * HTTP responses counter.
   *
   * @param name The name of the counter, without prefix and suffix.
   * @return A counter of http responses, identified by the given name. Counters with the same name are shared.
   */
  public @NotNull Counter httpResponses(@NotNull String name) {
    return counters.computeIfAbsent("vertx_" + name + "_responses", key -> register(Counter.build(key, "HTTP responses number")
        .labelNames("local_address", "code").create()));
  }

  private @NotNull Counter register(@NotNull Counter counter) {
    registry.register(counter);
    return counter;
  }
}