/*
* Copyright 2015 LinkedIn Corp. All rights reserved.
*
* 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.
*/

package com.linkedin.android.spyglass.sample.samples;

import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.linkedin.android.spyglass.sample.R;
import com.linkedin.android.spyglass.sample.data.models.City;
import com.linkedin.android.spyglass.sample.data.models.Person;
import com.linkedin.android.spyglass.suggestions.SuggestionsResult;
import com.linkedin.android.spyglass.suggestions.impl.BasicSuggestionsListBuilder;
import com.linkedin.android.spyglass.suggestions.interfaces.Suggestible;
import com.linkedin.android.spyglass.suggestions.interfaces.SuggestionsResultListener;
import com.linkedin.android.spyglass.tokenization.QueryToken;
import com.linkedin.android.spyglass.tokenization.interfaces.QueryTokenReceiver;
import com.linkedin.android.spyglass.ui.RichEditorView;

import java.util.ArrayList;
import java.util.List;

/**
 * Demonstrates how mentions are handled when there is a delay retrieving the suggestions (i.e. over a network).
 */
public class MultiSourceMentions extends AppCompatActivity implements QueryTokenReceiver {

    private static final String PERSON_BUCKET = "people-database";
    private static final String CITY_BUCKET = "city-network";
    private static final int PERSON_DELAY = 10;
    private static final int CITY_DELAY = 2000;

    private RichEditorView editor;
    private CheckBox peopleCheckBox;
    private CheckBox citiesCheckBox;

    private Person.PersonLoader people;
    private City.CityLoader cities;

    private SuggestionsResult lastPersonSuggestions;
    private SuggestionsResult lastCitySuggestions;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.multi_source_mentions);

        editor = (RichEditorView) findViewById(R.id.editor);
        editor.setQueryTokenReceiver(this);
        editor.setSuggestionsListBuilder(new CustomSuggestionsListBuilder());

        people = new Person.PersonLoader(getResources());
        cities = new City.CityLoader(getResources());

        peopleCheckBox = (CheckBox) findViewById(R.id.person_checkbox);
        peopleCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                updateSuggestions();
            }
        });

        citiesCheckBox = (CheckBox) findViewById(R.id.city_checkbox);
        citiesCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                updateSuggestions();
            }
        });

        updateSuggestions();
    }

    private void updateSuggestions() {
        final boolean hasPeople = peopleCheckBox.isChecked();
        final boolean hasCities = citiesCheckBox.isChecked();

        // Handle person mentions
        if (hasPeople && lastPersonSuggestions != null) {
            editor.onReceiveSuggestionsResult(lastPersonSuggestions, PERSON_BUCKET);
        } else if(lastPersonSuggestions != null) {
            SuggestionsResult emptySuggestions = new SuggestionsResult(lastPersonSuggestions.getQueryToken(), new ArrayList<Person>());
            editor.onReceiveSuggestionsResult(emptySuggestions, PERSON_BUCKET);
        }

        // Handle city mentions
        if (hasCities && lastCitySuggestions != null) {
            editor.onReceiveSuggestionsResult(lastCitySuggestions, CITY_BUCKET);
        } else if(lastCitySuggestions != null) {
            SuggestionsResult emptySuggestions = new SuggestionsResult(lastCitySuggestions.getQueryToken(), new ArrayList<City>());
            editor.onReceiveSuggestionsResult(emptySuggestions, CITY_BUCKET);
        }

        // Update the hint
        if (hasPeople && hasCities) {
            editor.setHint(getResources().getString(R.string.type_both));
        } else if (hasPeople && !hasCities) {
            editor.setHint(getResources().getString(R.string.type_person));
        } else if (!hasPeople && hasCities) {
            editor.setHint(getResources().getString(R.string.type_city));
        } else {
            editor.setHint(getResources().getString(R.string.type_neither));
        }
    }

    // --------------------------------------------------
    // QueryTokenReceiver Implementation
    // --------------------------------------------------

    @Override
    public List<String> onQueryReceived(final @NonNull QueryToken queryToken) {
        final boolean hasPeople = peopleCheckBox.isChecked();
        final boolean hasCities = citiesCheckBox.isChecked();

        final List<String> buckets = new ArrayList<>();
        final SuggestionsResultListener listener = editor;
        final Handler handler = new Handler(Looper.getMainLooper());

        // Fetch people if necessary
        if (hasPeople) {
            buckets.add(PERSON_BUCKET);
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    List<Person> suggestions = people.getSuggestions(queryToken);
                    lastPersonSuggestions = new SuggestionsResult(queryToken, suggestions);
                    listener.onReceiveSuggestionsResult(lastPersonSuggestions, PERSON_BUCKET);
                }
            }, PERSON_DELAY);
        }

        // Fetch cities if necessary
        if (hasCities) {
            buckets.add(CITY_BUCKET);
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    List<City> suggestions = cities.getSuggestions(queryToken);
                    lastCitySuggestions = new SuggestionsResult(queryToken, suggestions);
                    listener.onReceiveSuggestionsResult(lastCitySuggestions, CITY_BUCKET);
                }
            }, CITY_DELAY);
        }

        // Return buckets, one for each source (serves as promise to editor that we will call
        // onReceiveSuggestionsResult at a later time)
        return buckets;
    }

    // --------------------------------------------------
    // Inner class to customize appearance of suggestions
    // --------------------------------------------------

    private class CustomSuggestionsListBuilder extends BasicSuggestionsListBuilder {

        @NonNull
        @Override
        public View getView(@NonNull Suggestible suggestion,
                            @Nullable View convertView,
                            ViewGroup parent,
                            @NonNull Context context,
                            @NonNull LayoutInflater inflater,
                            @NonNull Resources resources) {

            View v =  super.getView(suggestion, convertView, parent, context, inflater, resources);
            if (!(v instanceof TextView)) {
                return v;
            }

            // Color text depending on the type of mention
            TextView tv = (TextView) v;
            if (suggestion instanceof Person) {
                tv.setTextColor(getResources().getColor(R.color.person_mention_text));
            } else if (suggestion instanceof City) {
                tv.setTextColor(getResources().getColor(R.color.city_mention_text));
            }

            return tv;
        }
    }
}


/*
* Copyright 2015 LinkedIn Corp. All rights reserved.
*
* 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.
*/

package com.linkedin.android.spyglass.mentions;

import android.os.Parcel;
import android.os.Parcelable;
import android.text.Editable;
import android.text.Selection;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.CharacterStyle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.linkedin.android.spyglass.ui.MentionsEditText;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Custom {@link Editable} containing methods specifically regarding mentions in a {@link Spanned} string object. Used
 * specifically within the {@link MentionsEditText}.
 */
public class MentionsEditable extends SpannableStringBuilder implements Parcelable {

    public MentionsEditable(@NonNull CharSequence text) {
        super(text);
    }

    public MentionsEditable(@NonNull CharSequence text, int start, int end) {
        super(text, start, end);
    }

    public MentionsEditable(@NonNull Parcel in) {
        super(in.readString());
        int length = in.readInt();
        if (length > 0) {
            for (int index = 0; index < length; index++) {
                int start = in.readInt();
                int end = in.readInt();
                MentionSpan span = new MentionSpan(in);
                setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }
    }

    // --------------------------------------------------
    // Overrides
    // --------------------------------------------------

    @Override
    public void setSpan(Object what, int start, int end, int flags) {
        // Do not add any spans that affect the character appearance of a mention (i.e. they overlap
        // with a MentionSpan). This helps prevent mentions from having a red underline due to the spell
        // checker. Note: SuggestionSpan was added in ICS, and different keyboards may use other kinds
        // of spans (i.e. the Motorola SpellCheckerMarkupSpan). Therefore, we cannot just filter out
        // SuggestionSpans, but rather, any span that would change the appearance of our MentionSpans.
        if (what instanceof CharacterStyle) {
            MentionSpan[] mentionSpans = getSpans(start, end, MentionSpan.class);
            if (mentionSpans != null && mentionSpans.length > 0) {
                return;
            }
        }

        // Ensure that the start and end points are set at zero initially
        // Note: This issue was seen on a Gingerbread device (start and end were both -1) and
        // prevents the device from crashing.
        if ((what == Selection.SELECTION_START || what == Selection.SELECTION_END) && length() == 0) {
            start = 0;
            end = 0;
        }

        super.setSpan(what, start, end, flags);
    }

    @NonNull
    @Override
    public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart, int tbend) {
        // On certain software keyboards, the editor appears to append a word minus the last character when it is really
        // trying to just delete the last character. Until we can figure out the root cause of this issue, the following
        // code remaps this situation to do a proper delete.
        if (start == end && start - tbend - 1 >= 0 && tb.length() > 1) {
            String insertString = tb.subSequence(tbstart, tbend).toString();
            String previousString = subSequence(start - tbend - 1, start - 1).toString();
            if (insertString.equals(previousString)) {
                // Delete a character
                return super.replace(start - 1, start, "", 0, 0);
            }
        }

        return super.replace(start, end, tb, tbstart, tbend);
    }

    // --------------------------------------------------
    // Custom Public Methods
    // --------------------------------------------------

    /**
     * Implementation of {@link String#trim()} for an {@link Editable}.
     *
     * @return a new {@link MentionsEditable} with whitespace characters removed from the beginning and the end
     */
    public MentionsEditable trim() {
        // Delete beginning spaces
        while (length() > 0 && Character.isWhitespace(charAt(0))) {
            delete(0, 1);
        }
        // Delete ending spaces
        int len = length();
        while (len > 0 && Character.isWhitespace(charAt(len - 1))) {
            delete(len - 1, len);
            len--;
        }
        return new MentionsEditable(this, 0, length());
    }

    @NonNull
    public List<MentionSpan> getMentionSpans() {
        MentionSpan[] mentionSpans = getSpans(0, length(), MentionSpan.class);
        return (mentionSpans != null) ? Arrays.asList(mentionSpans) : new ArrayList<MentionSpan>();
    }

    /**
     * Given an integer offset, return the {@link MentionSpan} located at the offset in the text of the
     * {@link android.widget.EditText}, if it exists. Otherwise, return null.
     *
     * @param index integer offset in text
     *
     * @return a {@link MentionSpan} located at index in text, or null
     */
    @Nullable
    public MentionSpan getMentionSpanAtOffset(int index) {
        MentionSpan[] spans = getSpans(index, index, MentionSpan.class);
        return (spans != null && spans.length > 0) ? spans[0] : null;
    }

    /**
     * Get the {@link MentionSpan} starting at the given index in the text, or null if there is no {@link MentionSpan}
     * starting at that index.
     *
     * @param index integer offset in text
     *
     * @return a {@link MentionSpan} starting at index in text, or null
     */
    @Nullable
    public MentionSpan getMentionSpanStartingAt(int index) {
        MentionSpan[] spans = getSpans(0, length(), MentionSpan.class);
        if (spans != null) {
            for (MentionSpan span : spans) {
                if (getSpanStart(span) == index) {
                    return span;
                }
            }
        }
        return null;
    }

    /**
     * Get the {@link MentionSpan} ending at the given index in the text, or null if there is no {@link MentionSpan}
     * ending at that index.
     *
     * @param index integer offset in text
     *
     * @return a {@link MentionSpan} ending at index in text, or null
     */
    @Nullable
    public MentionSpan getMentionSpanEndingAt(int index) {
        MentionSpan[] spans = getSpans(0, length(), MentionSpan.class);
        if (spans != null) {
            for (MentionSpan span : spans) {
                if (getSpanEnd(span) == index) {
                    return span;
                }
            }
        }
        return null;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(toString());
        int length = getMentionSpans().size();
        dest.writeInt(length);
        if (length > 0) {
            for (int index = 0; index < length; index++) {
                MentionSpan span = getMentionSpans().get(index);
                dest.writeInt(getSpanStart(span));
                dest.writeInt(getSpanEnd(span));
                span.writeToParcel(dest, flags);
            }
        }
    }

    public static final Parcelable.Creator<MentionsEditable> CREATOR
            = new Parcelable.Creator<MentionsEditable>() {
        public MentionsEditable createFromParcel(Parcel in) {
            return new MentionsEditable(in);
        }

        public MentionsEditable[] newArray(int size) {
            return new MentionsEditable[size];
        }
    };
}


/*
* Copyright 2015 LinkedIn Corp. All rights reserved.
*
* 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.
*/

package com.linkedin.android.spyglass.sample.data.models;

import android.content.res.Resources;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import androidx.annotation.NonNull;

import com.linkedin.android.spyglass.mentions.Mentionable;
import com.linkedin.android.spyglass.sample.R;
import com.linkedin.android.spyglass.sample.data.MentionsLoader;

import org.json.JSONArray;

/**
 * Model representing a basic, mentionable city.
 */
public class City implements Mentionable {

    private final String mName;

    public City(String name) {
        mName = name;
    }

    public String getName() {
        return mName;
    }

    // --------------------------------------------------
    // Mentionable Implementation
    // --------------------------------------------------

    @NonNull
    @Override
    public String getTextForDisplayMode(MentionDisplayMode mode) {
        switch (mode) {
            case FULL:
                return mName;
            case PARTIAL:
            case NONE:
            default:
                return "";
        }
    }

    @Override
    public MentionDeleteStyle getDeleteStyle() {
        // Note: Cities do not support partial deletion
        // i.e. "San Francisco" -> DEL -> ""
        return MentionDeleteStyle.PARTIAL_NAME_DELETE;
    }

    @Override
    public int getSuggestibleId() {
        return mName.hashCode();
    }

    @Override
    public String getSuggestiblePrimaryText() {
        return mName;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(mName);
    }

    public City(Parcel in) {
        mName = in.readString();
    }

    public static final Parcelable.Creator<City> CREATOR
            = new Parcelable.Creator<City>() {
        public City createFromParcel(Parcel in) {
            return new City(in);
        }

        public City[] newArray(int size) {
            return new City[size];
        }
    };

    // --------------------------------------------------
    // CityLoader Class (loads cities from JSON file)
    // --------------------------------------------------

    public static class CityLoader extends MentionsLoader<City> {
        private static final String TAG = CityLoader.class.getSimpleName();

        public CityLoader(Resources res) {
            super(res, R.raw.us_cities);
        }

        @Override
        public City[] loadData(JSONArray arr) {
            City[] data = new City[arr.length()];
            try {
                for (int i = 0; i < arr.length(); i++) {
                    data[i] = new City(arr.getString(i));
                }
            } catch (Exception e) {
                Log.e(TAG, "Unhandled exception while parsing city JSONArray", e);
            }
            return data;
        }
    }

}


package io.dropwizard.jobs.parser;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/*
 * This code is ripped of the playframework, see original here
 * https://github.com/playframework/play/blob/master/framework/src/play/libs/Time.java
 */
public class TimeParserUtil {

    private static Pattern days = Pattern.compile("^([0-9]+)d$");
    private static Pattern hours = Pattern.compile("^([0-9]+)h$");
    private static Pattern minutes = Pattern.compile("^([0-9]+)mi?n$");
    private static Pattern seconds = Pattern.compile("^([0-9]+)s$");
    private static Pattern milliseconds = Pattern.compile("^([0-9]+)ms$");

    /**
     * Parse a duration
     *
     * @param duration
     *            1d, 3h, 2mn, 7s, 50ms
     * @return The number of milliseconds
     */
    public static long parseDuration(String duration) {
        if (duration == null || duration.isEmpty()) {
            throw new IllegalArgumentException("duration may not be null");
        }
        long toAdd = -1;
        if (days.matcher(duration).matches()) {
            Matcher matcher = days.matcher(duration);
            matcher.matches();
            toAdd = Long.parseLong(matcher.group(1)) * 60 * 60 * 24 * 1000;
        } else if (hours.matcher(duration).matches()) {
            Matcher matcher = hours.matcher(duration);
            matcher.matches();
            toAdd = Long.parseLong(matcher.group(1)) * 60 * 60 * 1000;
        } else if (minutes.matcher(duration).matches()) {
            Matcher matcher = minutes.matcher(duration);
            matcher.matches();
            toAdd = Long.parseLong(matcher.group(1)) * 60 * 1000;
        } else if (seconds.matcher(duration).matches()) {
            Matcher matcher = seconds.matcher(duration);
            matcher.matches();
            toAdd = Long.parseLong(matcher.group(1)) * 1000;
        } else if (milliseconds.matcher(duration).matches()) {
            Matcher matcher = milliseconds.matcher(duration);
            matcher.matches();
            toAdd = Long.parseLong(matcher.group(1));
        }
        if (toAdd == -1) {
            throw new IllegalArgumentException("Invalid duration pattern : " + duration);
        }
        return toAdd;
    }

}


package io.dropwizard.jobs;

import io.dropwizard.jobs.annotations.*;
import io.dropwizard.jobs.parser.TimeParserUtil;
import io.dropwizard.lifecycle.Managed;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.WordUtils;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.spi.JobFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.ZoneId;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.TimeZone;
import java.util.stream.Collectors;

public class JobManager implements Managed {

    protected static final Logger log = LoggerFactory.getLogger(JobManager.class);

    protected Job[] jobs;
    protected Scheduler scheduler;
    protected JobConfiguration configuration;

    protected TimeZone defaultTimezone;

    public JobManager(JobConfiguration configuration, Job... jobs) {
        this.configuration = configuration;
        this.jobs = jobs;
        if (configuration != null && configuration.getQuartzConfiguration().containsKey("de.spinscale.dropwizard.jobs.timezone")) {
            defaultTimezone = TimeZone.getTimeZone(configuration.getQuartzConfiguration().get("de.spinscale.dropwizard.jobs.timezone"));
        } else {
            defaultTimezone = TimeZone.getDefault();
        }
    }

    @Override
    public void start() throws Exception {
        createScheduler();
        scheduler.setJobFactory(getJobFactory());
        scheduler.start();
        scheduleAllJobs();
        logAllOnApplicationStopJobs();
    }

    @Override
    public void stop() throws Exception {
        scheduleAllJobsOnApplicationStop();

        // this is enough to put the job into the queue, otherwise the jobs wont
        // be executed, anyone got a better solution?
        Thread.sleep(100);

        scheduler.shutdown(true);
    }

    public Scheduler getScheduler() {
        return scheduler;
    }

    protected JobFactory getJobFactory() {
        return new DropwizardJobFactory(jobs);
    }

    protected void scheduleAllJobs() throws SchedulerException {
        scheduleAllJobsOnApplicationStart();
        scheduleAllJobsWithEveryAnnotation();
        scheduleAllJobsWithOnAnnotation();
    }

    protected void scheduleAllJobsOnApplicationStop() throws SchedulerException {
        List<JobDetail> jobDetails = Arrays.stream(jobs)
            .filter(job -> job.getClass().isAnnotationPresent(OnApplicationStop.class))
            .map(job -> JobBuilder
                .newJob(job.getClass())
                .withIdentity(job.getClass().getName(), job.getGroupName())
                .build())
            .collect(Collectors.toList());
        for (JobDetail jobDetail : jobDetails) {
            scheduler.scheduleJob(jobDetail, executeNowTrigger());
        }
    }

    /**
     * Allow timezone to be configured on a per-cron basis with [timezoneName] appended to the cron format
     * @param cronExpr  the modified cron format
     * @return  the cron schedule with the timezone applied to it if needed
     */
    protected CronScheduleBuilder createCronScheduleBuilder(String cronExpr) {
        int i = cronExpr.indexOf("[");
        int j = cronExpr.indexOf("]");
        TimeZone timezone = defaultTimezone;
        if (i > -1 && j > -1) {
            timezone = TimeZone.getTimeZone(cronExpr.substring(i+1, j));
            cronExpr = cronExpr.substring(0, i).trim();
        }
        return CronScheduleBuilder.cronSchedule(cronExpr).inTimeZone(timezone);
    }

    protected void scheduleAllJobsWithOnAnnotation() throws SchedulerException {
        List<Job> onJobs = Arrays.stream(this.jobs)
            .filter(job -> job.getClass().isAnnotationPresent(On.class))
            .collect(Collectors.toList());

        if (onJobs.isEmpty()) {
            return;
        }

        log.info("Jobs with @On annotation:");
        for (Job job : onJobs) {
            Class<? extends Job> clazz = job.getClass();
            On onAnnotation = clazz.getAnnotation(On.class);
            String cron = onAnnotation.value();

            if(cron.isEmpty() || cron.matches("\\$\\{.*\\}")) {
                cron = this.readDurationFromConfig(onAnnotation, clazz);
                if (cron == null) {
                    throw new SchedulerConfigException("Missing config for " + clazz.getSimpleName());
                }
                log.info(clazz + " is configured in the config file to run every " + cron);
            }

            int priority = onAnnotation.priority();
            On.MisfirePolicy misfirePolicy = onAnnotation.misfirePolicy();
            boolean requestRecovery = onAnnotation.requestRecovery();
            boolean storeDurably = onAnnotation.storeDurably();

            CronScheduleBuilder scheduleBuilder = createCronScheduleBuilder(cron);

            String timeZoneStr = onAnnotation.timeZone();
            if (StringUtils.isNotBlank(timeZoneStr)) {
                TimeZone timeZone = TimeZone.getTimeZone(ZoneId.of(timeZoneStr));
                scheduleBuilder = scheduleBuilder.inTimeZone(timeZone);
            }

            if (misfirePolicy == On.MisfirePolicy.IGNORE_MISFIRES)
                scheduleBuilder.withMisfireHandlingInstructionIgnoreMisfires();
            else if (misfirePolicy == On.MisfirePolicy.DO_NOTHING)
                scheduleBuilder.withMisfireHandlingInstructionDoNothing();
            else if (misfirePolicy == On.MisfirePolicy.FIRE_AND_PROCEED)
                scheduleBuilder.withMisfireHandlingInstructionFireAndProceed();

            Trigger trigger = TriggerBuilder.newTrigger().withSchedule(scheduleBuilder).withPriority(priority).build();

            // ensure that only one instance of each job is scheduled
            JobKey jobKey = createJobKey(onAnnotation.jobName(), job);

            createOrUpdateJob(jobKey, clazz, trigger, requestRecovery, storeDurably);
            log.info(String.format("    %-21s %s", cron, jobKey.toString()));
        }
    }

    private JobKey createJobKey(final String annotationJobName, final Job job) {
        String key = StringUtils.isNotBlank(annotationJobName) ? annotationJobName : job.getClass().getCanonicalName();
        return JobKey.jobKey(key, job.getGroupName());
    }

    protected void scheduleAllJobsWithEveryAnnotation() throws SchedulerException {
        List<Job> everyJobs = Arrays.stream(this.jobs)
            .filter(job -> job.getClass().isAnnotationPresent(Every.class))
            .collect(Collectors.toList());

        if (everyJobs.isEmpty()) {
            return;
        }

        log.info("Jobs with @Every annotation:");
        for (Job job : everyJobs) {
            Class<? extends Job> clazz = job.getClass();
            Every everyAnnotation = clazz.getAnnotation(Every.class);
            int priority = everyAnnotation.priority();
            Every.MisfirePolicy misfirePolicy = everyAnnotation.misfirePolicy();
            boolean requestRecovery = everyAnnotation.requestRecovery();
            boolean storeDurably = everyAnnotation.storeDurably();
            int repeatCount = everyAnnotation.repeatCount();

            String value = everyAnnotation.value();
            if (value.isEmpty() || value.matches("\\$\\{.*\\}")) {
                value = readDurationFromConfig(everyAnnotation, clazz);
                log.info(clazz + " is configured in the config file to run every " + value);
            }
            long milliSeconds = TimeParserUtil.parseDuration(value);

            SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
                    .withIntervalInMilliseconds(milliSeconds);

            if (repeatCount > -1)
                scheduleBuilder.withRepeatCount(repeatCount);
            else
                scheduleBuilder.repeatForever();

            if (misfirePolicy == Every.MisfirePolicy.IGNORE_MISFIRES)
                scheduleBuilder.withMisfireHandlingInstructionIgnoreMisfires();
            else if (misfirePolicy == Every.MisfirePolicy.FIRE_NOW)
                scheduleBuilder.withMisfireHandlingInstructionFireNow();
            else if (misfirePolicy == Every.MisfirePolicy.NOW_WITH_EXISTING_COUNT)
                scheduleBuilder.withMisfireHandlingInstructionNowWithExistingCount();
            else if (misfirePolicy == Every.MisfirePolicy.NOW_WITH_REMAINING_COUNT)
                scheduleBuilder.withMisfireHandlingInstructionNowWithRemainingCount();
            else if (misfirePolicy == Every.MisfirePolicy.NEXT_WITH_EXISTING_COUNT)
                scheduleBuilder.withMisfireHandlingInstructionNextWithExistingCount();
            else if (misfirePolicy == Every.MisfirePolicy.NEXT_WITH_REMAINING_COUNT)
                scheduleBuilder.withMisfireHandlingInstructionNextWithRemainingCount();

            DateTime start = new DateTime();
            DelayStart delayAnnotation = clazz.getAnnotation(DelayStart.class);
            if (delayAnnotation != null) {
                long milliSecondDelay = TimeParserUtil.parseDuration(delayAnnotation.value());
                start = start.plus(Duration.millis(milliSecondDelay));
            }

            Trigger trigger = TriggerBuilder.newTrigger().withSchedule(scheduleBuilder)
                    .startAt(start.toDate())
                    .withPriority(priority)
                    .build();

            // ensure that only one instance of each job is scheduled
            JobKey jobKey = createJobKey(everyAnnotation.jobName(), job);
            createOrUpdateJob(jobKey, clazz, trigger, requestRecovery, storeDurably);

            String logMessage = String.format("    %-7s %s", everyAnnotation.value(), jobKey.toString());
            if (delayAnnotation != null) {
                logMessage += " (" + delayAnnotation.value() + " delay)";
            }
            log.info(logMessage);
        }
    }

    private String readDurationFromConfig(On annotation, Class<? extends org.quartz.Job> clazz) {
        if(this.configuration == null) {
            return null;
        } else {
            String property = WordUtils.uncapitalize(clazz.getSimpleName());
            if(!annotation.value().isEmpty()) {
                property = annotation.value().substring(2, annotation.value().length() - 1);
            }
            return (String)this.configuration.getJobs().getOrDefault(property,  null);
        }
    }

    private String readDurationFromConfig(Every annotation, Class<? extends org.quartz.Job> clazz) {
        if (configuration == null) {
            return null;
        }
        String property = WordUtils.uncapitalize(clazz.getSimpleName());
        if (!annotation.value().isEmpty()) {
            property = annotation.value().substring(2, annotation.value().length() - 1);
        }
        return configuration.getJobs().getOrDefault(property, null);
    }

    protected void scheduleAllJobsOnApplicationStart() throws SchedulerException {
        List<JobDetail> jobDetails = Arrays.stream(this.jobs)
            .filter(job -> job.getClass().isAnnotationPresent(OnApplicationStart.class))
            .map(job -> JobBuilder
                .newJob(job.getClass())
                .withIdentity(job.getClass().getName(), job.getGroupName())
                .build())
            .collect(Collectors.toList());

        if (!jobDetails.isEmpty()) {
            log.info("Jobs to run on application start:");
            for (JobDetail jobDetail : jobDetails) {
                scheduler.scheduleJob(jobDetail, executeNowTrigger());
                log.info("   " + jobDetail.getJobClass().getCanonicalName());
            }
        }
    }

    protected Trigger executeNowTrigger() {
        return TriggerBuilder.newTrigger().startNow().build();
    }

    private void createScheduler() throws SchedulerException {
        if (configuration.getQuartzConfiguration().isEmpty()) {
            scheduler = StdSchedulerFactory.getDefaultScheduler();
            return;
        }

        StdSchedulerFactory factory = new StdSchedulerFactory();
        factory.initialize(createProperties());
        scheduler = factory.getScheduler();
    }

    private Properties createProperties() {
        Properties properties = new Properties();
        properties.putAll(configuration.getQuartzConfiguration());
        return properties;
    }

    private void logAllOnApplicationStopJobs() {
        log.info("Jobs to run on application stop:");

        Arrays.stream(this.jobs)
                .filter(job -> job.getClass().isAnnotationPresent(OnApplicationStop.class))
                .map(job -> job.getClass())
                .forEach(clazz -> log.info("   " + clazz.getCanonicalName()));
    }

    private void createOrUpdateJob(JobKey jobKey, Class<? extends org.quartz.Job> clazz, Trigger trigger, boolean requestsRecovery,
            boolean storeDurably) throws SchedulerException {
        JobBuilder jobBuilder = JobBuilder.newJob(clazz).withIdentity(jobKey).requestRecovery(requestsRecovery)
                .storeDurably(storeDurably);

        if (!scheduler.checkExists(jobKey)) {
            // if the job doesn't already exist, we can create it, along with its trigger. this prevents us
            // from creating multiple instances of the same job when running in a clustered environment
            scheduler.scheduleJob(jobBuilder.build(), trigger);
            log.info("scheduled job with key " + jobKey.toString());
        } else {
            // if the job has exactly one trigger, we can just reschedule it, which allows us to update the schedule for
            // that trigger.
            List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
            if (triggers.size() == 1) {
                scheduler.rescheduleJob(triggers.get(0).getKey(), trigger);
            } else {
                // if for some reason the job has multiple triggers, it's easiest to just delete and re-create the job,
                // since we want to enforce a one-to-one relationship between jobs and triggers
                scheduler.deleteJob(jobKey);
                scheduler.scheduleJob(jobBuilder.build(), trigger);
            }
        }
    }
}


/* ===================================================
 * JFreeSVG : an SVG library for the Java(tm) platform
 * ===================================================
 *
 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
 *
 * Project Info:  http://www.jfree.org/jfreesvg/index.html
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.]
 *
 * If you do not wish to be bound by the terms of the GPL, an alternative
 * commercial license can be purchased.  For details, please see visit the
 * JFreeSVG home page:
 *
 * http://www.jfree.org/jfreesvg
 */

package org.jfree.graphics2d.svg;

import java.awt.RenderingHints;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

/**
 * Defines the rendering hints that can be used with the {@link SVGGraphics2D}
 * class.  The supported hints are:

 *

 *
{@link #KEY_IMAGE_HANDLING} that controls how images are handled
 * (embedded in the SVG, or referenced externally);

 *
{@link #KEY_IMAGE_HREF} that allows the caller to specify the image
 * href attribute for the next image;

 *
{@link #KEY_TEXT_RENDERING} that allows configuration of the preferred
 * value of the SVG {@code text-rendering} attribute in text elements;

 *
{@link #KEY_ELEMENT_ID} that allows the caller to specify the element
 * ID for the next element;

 *
{@link #KEY_BEGIN_GROUP} tells the {@code SVGGraphics2D} instance
 * to start a new group element with an id equal to the hint value (which must
 * be an instance of String).  Any other {@code Graphics2D} implementation
 * will ignore this hint;

 *
{@link #KEY_END_GROUP} tells the {@code SVGGraphics2D} instance
 * to end a group element.  The hint value is ignored.  The caller assumes
 * responsibility for balancing the number of {@code KEY_BEGIN_GROUP} and
 * {@code KEY_END_GROUP} hints.  Any other {@code Graphics2D}
 * implementation will ignore this hint.

 *

 *
 */
public final class SVGHints {

    private SVGHints() {
        // no need to instantiate this
    }

    /**
     * The key for the hint that controls whether images are embedded in the
     * SVG or referenced externally.  Valid hint values are
     * {@link #VALUE_IMAGE_HANDLING_EMBED} and
     * {@link #VALUE_IMAGE_HANDLING_REFERENCE}.
     */
    public static final SVGHints.Key KEY_IMAGE_HANDLING = new SVGHints.Key(0);

    /**
     * Hint value for {@code KEY_IMAGE_HANDLING} to specify that images
     * should be embedded in the SVG output using PNG data {@code Base64}
     * encoded.
     */
    public static final Object VALUE_IMAGE_HANDLING_EMBED
            = "VALUE_IMAGE_HANDLING_EMBED";

    /**
     * Hint value for {@code KEY_IMAGE_HANDLING} to say that images should
     * be referenced externally.
     */
    public static final Object VALUE_IMAGE_HANDLING_REFERENCE
            = "VALUE_IMAGE_HANDLING_REFERENCE";

    /**
     * The key for a hint that permits configuration of the text-rendering
     * attribute in SVG text elements
     */
    public static final SVGHints.Key KEY_TEXT_RENDERING = new SVGHints.Key(1);

    /**
     * Hint value for {@code KEY_TEXT_RENDERING} to set the
     * {@code text-rendering} attribute in SVG text elements to 'auto'.
     */
    public static final String VALUE_TEXT_RENDERING_AUTO = "auto";

    /**
     * Hint value for {@code KEY_TEXT_RENDERING} to set the
     * {@code text-rendering} attribute in SVG text elements to
     * 'optimizeSpeed'.
     */
    public static final String VALUE_TEXT_RENDERING_SPEED = "optimizeSpeed";

    /**
     * Hint value for {@code KEY_TEXT_RENDERING} to set the
     * {@code text-rendering} attribute in SVG text elements to
     * 'optimizeLegibility'.
     */
    public static final String VALUE_TEXT_RENDERING_LEGIBILITY
            = "optimizeLegibility";

    /**
     * Hint value for {@code KEY_TEXT_RENDERING} to set the
     * {@code text-rendering} attribute in SVG text elements to
     * 'geometricPrecision'.
     */
    public static final String VALUE_TEXT_RENDERING_PRECISION
            = "geometricPrecision";

    /**
     * Hint value for {@code KEY_TEXT_RENDERING} to set the
     * {@code text-rendering} attribute in SVG text elements to
     * 'inherit'.
     */
    public static final String VALUE_TEXT_RENDERING_INHERIT = "inherit";

    /**
     * Hint key to supply string to be used as the href for an image that is
     * referenced rather than embedded.  The value associated with the key
     * should be a string and will be used for the next image element written
     * to the SVG output (and then the hint will be cleared).
     *
     * @since 1.5
     */
    public static final SVGHints.Key KEY_IMAGE_HREF = new SVGHints.Key(2);

    /**
     * Hint key to supply an element id for the next element generated.
     *
     * @since 1.5
     */
    public static final SVGHints.Key KEY_ELEMENT_ID = new SVGHints.Key(3);

    /**
     * Hint key that informs the {@code SVGGraphics2D} that the caller
     * would like to begin a new group element.  The hint value is the id for
     * the new group.  After opening the new group the hint is cleared and it
     * is the caller's responsibility to close the group later using
     * {@link SVGHints#KEY_END_GROUP}.  Groups can be nested.
     *
     * @since 1.7
     */
    public static final SVGHints.Key KEY_BEGIN_GROUP = new SVGHints.Key(4);

    /**
     * Hint key that informs the {@code SVGGraphics2D} that the caller
     * would like to close a previously opened group element.  The hint
     * value is ignored.
     *
     * @since 1.7
     */
    public static final SVGHints.Key KEY_END_GROUP = new SVGHints.Key(5);

    /**
     * Hint key that informs the {@code SVGGraphics2D} that the caller
     * would like to add a title element to the output (with the hint value
     * being a string containing the title text).
     *
     * @since 1.9
     */
    public static final SVGHints.Key KEY_ELEMENT_TITLE = new SVGHints.Key(6);

    /**
     * The key for the hint that controls whether strings are rendered as
     * characters or vector graphics (implemented using {@code TextLayout}).
     * The latter will result in larger output files but avoids problems with
     * fonts not being available for the viewer.  Valid hint values are
     * {@link #VALUE_DRAW_STRING_TYPE_STANDARD} and
     * {@link #VALUE_DRAW_STRING_TYPE_VECTOR}.
     *
     * @since 2.0
     */
    public static final SVGHints.Key KEY_DRAW_STRING_TYPE = new SVGHints.Key(7);

    /**
     * Hint value for {@code KEY_DRAW_STRING_TYPE} to specify that strings
     * should be written to the output using standard SVG text elements.
     *
     * @since 2.0
     */
    public static final Object VALUE_DRAW_STRING_TYPE_STANDARD
            = "VALUE_DRAW_STRING_TYPE_STANDARD";

    /**
     * Hint value for {@code KEY_DRAW_STRING_TYPE} to say that strings
     * should be written to the output using vector graphics primitives.
     *
     * @since 2.0
     */
    public static final Object VALUE_DRAW_STRING_TYPE_VECTOR
            = "VALUE_DRAW_STRING_TYPE_VECTOR";

    /**
     * A list of keys that are treated as synonyms for KEY_BEGIN_GROUP
     * (the list does not include KEY_BEGIN_GROUP itself).
     */
    private static final List<RenderingHints.Key> beginGroupKeys;

    /**
     * A list of keys that are treated as synonyms for KEY_END_GROUP
     * (the list does not include KEY_END_GROUP itself).
     */
    private static final List<RenderingHints.Key> endGroupKeys;

    /**
     * A list of keys that are treated as synonyms for KEY_ELEMENT_TITLE
     * (the list does not include KEY_ELEMENT_TITLE itself).
     */
    private static final List<RenderingHints.Key> elementTitleKeys;

    static {
        beginGroupKeys = new ArrayList<RenderingHints.Key>();
        endGroupKeys = new ArrayList<RenderingHints.Key>();
        elementTitleKeys = new ArrayList<RenderingHints.Key>();
        if (isOrsonChartsOnClasspath()) {
            beginGroupKeys.add(getOrsonChartsBeginElementKey());
            endGroupKeys.add(getOrsonChartsEndElementKey());
            elementTitleKeys.add(getOrsonChartsElementTitleKey());
        }
        if (isJFreeChartOnClasspath()) {
            beginGroupKeys.add(getJFreeChartBeginElementKey());
            endGroupKeys.add(getJFreeChartEndElementKey());
        }
    }

    /**
     * Creates and returns a list of keys that are synonymous with
     * {@link #KEY_BEGIN_GROUP}.
     *
     * @return A list (never {@code null}).
     *
     * @since 1.8
     */
    public static List<RenderingHints.Key> getBeginGroupKeys() {
        return new ArrayList<RenderingHints.Key>(beginGroupKeys);
    }

    /**
     * Adds a key to the list of keys that are synonyms for
     * {@link SVGHints#KEY_BEGIN_GROUP}.
     *
     * @param key  the key ({@code null} not permitted).
     *
     * @since 1.8
     */
    public static void addBeginGroupKey(RenderingHints.Key key) {
        beginGroupKeys.add(key);
    }

    /**
     * Removes a key from the list of keys that are synonyms for
     * {@link SVGHints#KEY_BEGIN_GROUP}.
     *
     * @param key  the key ({@code null} not permitted).
     *
     * @since 1.8
     */
    public static void removeBeginGroupKey(RenderingHints.Key key) {
        beginGroupKeys.remove(key);
    }

    /**
     * Clears the list of keys that are treated as synonyms for
     * {@link SVGHints#KEY_BEGIN_GROUP}.
     *
     * @since 1.8
     */
    public static void clearBeginGroupKeys() {
        beginGroupKeys.clear();
    }

    /**
     * Returns {@code true} if this key is equivalent to
     * {@link #KEY_BEGIN_GROUP}, and {@code false} otherwise.  The purpose
     * of this method is to allow certain keys from external packages (such as
     * JFreeChart and Orson Charts) to use their own keys to drive the
     * behaviour of {@code SVGHints.KEY_BEGIN_GROUP}.  This has two benefits:
     * (1) it avoids the necessity to make JFreeSVG a direct dependency, and
     * (2) it makes the grouping behaviour generic from the point of view of
     * the external package, rather than SVG-specific.
     *
     * @param key  the key ({@code null} not permitted)
     *
     * @return A boolean.
     *
     * @since 1.8
     */
    public static boolean isBeginGroupKey(RenderingHints.Key key) {
        return SVGHints.KEY_BEGIN_GROUP.equals(key)
                || beginGroupKeys.contains(key);
    }

    /**
     * Creates and returns a list of keys that are synonymous with
     * {@link #KEY_END_GROUP}.
     *
     * @return A list (never {@code null}).
     *
     * @since 1.8
     */
    public static List<RenderingHints.Key> getEndGroupKeys() {
        return new ArrayList<RenderingHints.Key>(endGroupKeys);
    }

    /**
     * Adds a key to the list of keys that are synonyms for
     * {@link SVGHints#KEY_END_GROUP}.
     *
     * @param key  the key ({@code null} not permitted).
     *
     * @since 1.8
     */
    public static void addEndGroupKey(RenderingHints.Key key) {
        endGroupKeys.add(key);
    }

    /**
     * Removes a key from the list of keys that are synonyms for
     * {@link SVGHints#KEY_END_GROUP}.
     *
     * @param key  the key ({@code null} not permitted).
     *
     * @since 1.8
     */
    public static void removeEndGroupKey(RenderingHints.Key key) {
        endGroupKeys.remove(key);
    }

    /**
     * Clears the list of keys that are treated as synonyms for
     * {@link SVGHints#KEY_END_GROUP}.
     *
     * @since 1.8
     */
    public static void clearEndGroupKeys() {
        endGroupKeys.clear();
    }

    /**
     * Returns {@code true} if this key is equivalent to
     * {@link #KEY_END_GROUP}, and {@code false} otherwise.  The purpose
     * of this method is to allow certain keys from external packages (such as
     * JFreeChart and Orson Charts) to use their own keys to drive the
     * behaviour of {@code SVGHints.KEY_END_GROUP}.  This has two benefits:
     * (1) it avoids the necessity to make JFreeSVG a direct dependency, and
     * (2) it makes the grouping behaviour generic from the point of view of
     * the external package, rather than SVG-specific.
     *
     * @param key  the key ({@code null} not permitted).
     *
     * @return A boolean.
     *
     * @since 1.8
     */
    public static boolean isEndGroupKey(RenderingHints.Key key) {
        return SVGHints.KEY_END_GROUP.equals(key) || endGroupKeys.contains(key);
    }

    /**
     * Creates and returns a list of keys that are synonymous with
     * {@link #KEY_ELEMENT_TITLE}.
     *
     * @return A list (never {@code null}).
     *
     * @since 1.9
     */
    public static List<RenderingHints.Key> getElementTitleKeys() {
        return new ArrayList<RenderingHints.Key>(elementTitleKeys);
    }

    /**
     * Adds a key to the list of keys that are synonyms for
     * {@link SVGHints#KEY_ELEMENT_TITLE}.
     *
     * @param key  the key ({@code null} not permitted).
     *
     * @since 1.9
     */
    public static void addElementTitleKey(RenderingHints.Key key) {
        elementTitleKeys.add(key);
    }

    /**
     * Removes a key from the list of keys that are synonyms for
     * {@link SVGHints#KEY_ELEMENT_TITLE}.
     *
     * @param key  the key ({@code null} not permitted).
     *
     * @since 1.9
     */
    public static void removeElementTitleKey(RenderingHints.Key key) {
        elementTitleKeys.remove(key);
    }

    /**
     * Clears the list of keys that are treated as synonyms for
     * {@link SVGHints#KEY_ELEMENT_TITLE}.
     *
     * @since 1.9
     */
    public static void clearElementTitleKeys() {
        elementTitleKeys.clear();
    }

    /**
     * Returns {@code true} if this key is equivalent to
     * {@link #KEY_ELEMENT_TITLE}, and {@code false} otherwise.  The
     * purpose of this method is to allow certain keys from external packages
     * (such as JFreeChart and Orson Charts) to use their own keys to drive the
     * behaviour of {@code SVGHints.KEY_ELEMENT_TITLE}.  This has two benefits:
     * (1) it avoids the necessity to make JFreeSVG a direct dependency, and
     * (2) it makes the element title behaviour generic from the point of view
     * of the external package, rather than SVG-specific.
     *
     * @param key  the key ({@code null} not permitted)
     *
     * @return A boolean.
     *
     * @since 1.9
     */
    public static boolean isElementTitleKey(RenderingHints.Key key) {
        return SVGHints.KEY_ELEMENT_TITLE.equals(key)
                || elementTitleKeys.contains(key);
    }

    /**
     * Returns {@code true} if Orson Charts (version 1.3 or later) is on
     * the classpath, and {@code false} otherwise.  This method is used to
     * auto-register keys from Orson Charts that should translate to the
     * behaviour of {@link SVGHints#KEY_BEGIN_GROUP} and
     * {@link SVGHints#KEY_END_GROUP}.
     *

     * The Orson Charts library can be found at
     * http://www.object-refinery.com/orsoncharts/
     *
     * @return A boolean.
     *
     * @since 1.8
     */
    private static boolean isOrsonChartsOnClasspath() {
        return (getOrsonChartsBeginElementKey() != null);
    }

    /**
     * Returns {@code true} if JFreeChart (1.0.18 or later) is on
     * the classpath, and {@code false} otherwise.  This method is used to
     * auto-register keys from JFreeChart that should translate to the
     * behaviour of {@link SVGHints#KEY_BEGIN_GROUP} and
     * {@link SVGHints#KEY_END_GROUP}.
     *
     *
The JFreeChart library can be found at
     * http://www.jfree.org/jfreechart/.
     *
     * @return A boolean.
     *
     * @since 2.0
     */
    private static boolean isJFreeChartOnClasspath() {
        return (getJFreeChartBeginElementKey() != null);
    }

    private static RenderingHints.Key fetchKey(String className,
            String fieldName) {
        Class<?> hintsClass;
        try {
            hintsClass = Class.forName(className);
            Field f = hintsClass.getDeclaredField(fieldName);
            return (RenderingHints.Key) f.get(null);
        } catch (ClassNotFoundException e) {
            return null;
        } catch (NoSuchFieldException ex) {
            return null;
        } catch (SecurityException ex) {
            return null;
        } catch (IllegalArgumentException ex) {
            return null;
        } catch (IllegalAccessException ex) {
            return null;
        }
    }

    private static RenderingHints.Key getOrsonChartsBeginElementKey() {
        return fetchKey("com.orsoncharts.Chart3DHints", "KEY_BEGIN_ELEMENT");
    }

    private static RenderingHints.Key getOrsonChartsEndElementKey() {
        return fetchKey("com.orsoncharts.Chart3DHints", "KEY_END_ELEMENT");
    }

    private static RenderingHints.Key getOrsonChartsElementTitleKey() {
        return fetchKey("com.orsoncharts.Chart3DHints", "KEY_ELEMENT_TITLE");
    }

    private static RenderingHints.Key getJFreeChartBeginElementKey() {
        return fetchKey("org.jfree.chart.ChartHints", "KEY_BEGIN_ELEMENT");
    }

    private static RenderingHints.Key getJFreeChartEndElementKey() {
        return fetchKey("org.jfree.chart.ChartHints", "KEY_END_ELEMENT");
    }

    /**
     * A key for hints used by the {@link SVGGraphics2D} class.
     */
    public static class Key extends RenderingHints.Key {

        /**
         * Creates a new instance.
         *
         * @param privateKey  the private key.
         */
        public Key(int privateKey) {
            super(privateKey);
        }

        /**
         * Returns {@code true} if {@code val} is a value that is
         * compatible with this key, and {@code false} otherwise.
         *
         * @param val  the value.
         *
         * @return A boolean.
         */
        @Override
        public boolean isCompatibleValue(Object val) {
            switch (intKey()) {
                case 0:
                    return VALUE_IMAGE_HANDLING_EMBED.equals(val)
                            || VALUE_IMAGE_HANDLING_REFERENCE.equals(val);
                case 1:
                    return VALUE_TEXT_RENDERING_AUTO.equals(val)
                            || VALUE_TEXT_RENDERING_INHERIT.equals(val)
                            || VALUE_TEXT_RENDERING_LEGIBILITY.equals(val)
                            || VALUE_TEXT_RENDERING_PRECISION.equals(val)
                            || VALUE_TEXT_RENDERING_SPEED.equals(val);
                case 2: // KEY_IMAGE:URL
                    return val == null || val instanceof String;
                case 3: // KEY_ELEMENT_ID
                    return val == null || val instanceof String;
                case 4: // KEY_BEGIN_GROUP
                    return val == null || val instanceof String;
                case 5: // KEY_END_GROUP
                    return true; // the value is ignored
                case 6: // KEY_ELEMENT_TITLE
                    return val instanceof String;
                case 7:
                    return val == null
                            || VALUE_DRAW_STRING_TYPE_STANDARD.equals(val)
                            || VALUE_DRAW_STRING_TYPE_VECTOR.equals(val);
                default:
                    throw new RuntimeException("Not possible!");
            }
        }
    }

}


/* ===================================================
 * JFreeSVG : an SVG library for the Java(tm) platform
 * ===================================================
 *
 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
 *
 * Project Info:  http://www.jfree.org/jfreesvg/index.html
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.]
 *
 * If you do not wish to be bound by the terms of the GPL, an alternative
 * commercial license can be purchased.  For details, please see visit the
 * JFreeSVG home page:
 *
 * http://www.jfree.org/jfreesvg
 *
 */

package org.jfree.graphics2d.svg;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPOutputStream;
import org.jfree.graphics2d.Args;

/**
 * Utility methods related to the {@link SVGGraphics2D} implementation.
 */
public class SVGUtils {

    private SVGUtils() {
        // no need to instantiate this
    }

    /**
     * Returns a new string where any special characters in the source string
     * have been encoded.
     *
     * @param source  the source string ({@code null} not permitted).
     *
     * @return A new string with special characters escaped for XML.
     *
     * @since 1.5
     */
    public static String escapeForXML(String source) {
        Args.nullNotPermitted(source, "source");
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < source.length(); i++) {
            char c = source.charAt(i);
            switch (c) {
                case '<' : {
                    sb.append("<");
                    break;
                }
                case '>' : {
                    sb.append(">");
                    break;
                }
                case '&' : {
                    String next = source.substring(i, Math.min(i + 6,
                            source.length()));
                    if (next.startsWith("<") || next.startsWith(">")
                            || next.startsWith("&")
                            || next.startsWith("'")
                            || next.startsWith(""")) {
                        sb.append(c);
                    } else {
                        sb.append("&");
                    }
                    break;
                }
                case '\'' : {
                    sb.append("'");
                    break;
                }
                case '\"' : {
                    sb.append(""");
                    break;
                }
                default : sb.append(c);
            }
        }
        return sb.toString();
    }

    /**
     * Writes a file containing the SVG element.
     *
     * @param file  the file ({@code null} not permitted).
     * @param svgElement  the SVG element ({@code null} not permitted).
     *
     * @throws IOException if there is an I/O problem.
     *
     * @since 1.2
     */
    public static void writeToSVG(File file, String svgElement)
            throws IOException {
        writeToSVG(file, svgElement, false);
    }

    /**
     * Writes a file containing the SVG element.
     *
     * @param file  the file ({@code null} not permitted).
     * @param svgElement  the SVG element ({@code null} not permitted).
     * @param zip  compress the output.
     *
     * @throws IOException if there is an I/O problem.
     *
     * @since 3.0
     */
    public static void writeToSVG(File file, String svgElement, boolean zip)
            throws IOException {
        BufferedWriter writer = null;
        try {
            OutputStream os = new FileOutputStream(file);
            if (zip) {
                os = new GZIPOutputStream(os);
            }
            OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8");
            writer = new BufferedWriter(osw);
            writer.write("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n");
            writer.write(svgElement + "\n");
            writer.flush();
        } finally {
            try {
                if (writer != null) {
                    writer.close();
                }
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
        }
    }

    /**
     * Writes an HTML file containing an SVG element.
     *
     * @param file  the file.
     * @param title  the title.
     * @param svgElement  the SVG element.
     *
     * @throws IOException if there is an I/O problem.
     */
    public static void writeToHTML(File file, String title, String svgElement)
            throws IOException {
        BufferedWriter writer = null;
        try {
            FileOutputStream fos = new FileOutputStream(file);
            OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
            writer = new BufferedWriter(osw);
            writer.write("<!DOCTYPE html>\n");
            writer.write("\n");
            writer.write("\n");
            writer.write("<title>" + title + "</title>\n");
            writer.write("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n");
            writer.write("\n");
            writer.write("\n");
            writer.write(svgElement + "\n");
            writer.write("\n");
            writer.write("\n");
            writer.flush();
        } finally {
            try {
                if (writer != null) {
                    writer.close();
                }
            } catch (IOException ex) {
                Logger.getLogger(SVGUtils.class.getName()).log(Level.SEVERE,
                        null, ex);
            }
        }
    }

}


/* ===================================================
 * JFreeSVG : an SVG library for the Java(tm) platform
 * ===================================================
 *
 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
 *
 * Project Info:  http://www.jfree.org/jfreesvg/index.html
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.]
 *
 * If you do not wish to be bound by the terms of the GPL, an alternative
 * commercial license can be purchased.  For details, please see visit the
 * JFreeSVG home page:
 *
 * http://www.jfree.org/jfreesvg
 *
 */

package org.jfree.graphics2d;

import java.awt.Polygon;
import java.awt.Shape;
import java.awt.geom.Arc2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.util.Hashtable;

/**
 * A collection of static utility methods for shapes and images.
 */
public final class GraphicsUtils {

    private GraphicsUtils() {
        // no need to instantiate this
    }

    /**
     * Returns a shape that is (more or less) equivalent to the supplied shape.
     * For some known shape implementations ({@code Line2D},
     * {@code Rectangle2D}, {@code RoundRectangle2D}, {@code Arc2D},
     * {@code Ellipse2D}, and {@code Polygon}) the copy will be an instance of
     * that class.  For other shapes, a {@code Path2D} containing the outline
     * of the shape is returned.
     *
     * @param shape  the shape ({@code null} not permitted).
     *
     * @return A copy of the shape or shape outline (never {@code null}).
     */
    public static Shape copyOf(Shape shape) {
       Args.nullNotPermitted(shape, "shape");
       if (shape instanceof Line2D) {
           Line2D l = (Line2D) shape;
           return new Line2D.Double(l.getX1(), l.getY1(), l.getX2(), l.getY2());
       }
       if (shape instanceof Rectangle2D) {
           Rectangle2D r = (Rectangle2D) shape;
           return new Rectangle2D.Double(r.getX(), r.getY(), r.getWidth(),
                   r.getHeight());
       }
       if (shape instanceof RoundRectangle2D) {
           RoundRectangle2D rr = (RoundRectangle2D) shape;
           return new RoundRectangle2D.Double(rr.getX(), rr.getY(),
                   rr.getWidth(), rr.getHeight(), rr.getArcWidth(),
                   rr.getArcHeight());
       }
       if (shape instanceof Arc2D) {
           Arc2D arc = (Arc2D) shape;
           return new Arc2D.Double(arc.getX(), arc.getY(), arc.getWidth(),
                   arc.getHeight(), arc.getAngleStart(), arc.getAngleExtent(),
                   arc.getArcType());
       }
       if (shape instanceof Ellipse2D) {
           Ellipse2D ell = (Ellipse2D) shape;
           return new Ellipse2D.Double(ell.getX(), ell.getY(), ell.getWidth(),
                   ell.getHeight());
       }
       if (shape instanceof Polygon) {
           Polygon p = (Polygon) shape;
           return new Polygon(p.xpoints, p.ypoints, p.npoints);
       }
       return new Path2D.Double(shape);
    }

    /**
     * Creates a polygon from the specified {@code x} and {@code y} coordinate
     * arrays.
     *
     * @param xPoints  the x-points.
     * @param yPoints  the y-points.
     * @param nPoints  the number of points to use for the polyline.
     * @param close  closed?
     *
     * @return A polygon.
     */
    public static GeneralPath createPolygon(int[] xPoints, int[] yPoints,
            int nPoints, boolean close) {
        GeneralPath p = new GeneralPath();
        p.moveTo(xPoints[0], yPoints[0]);
        for (int i = 1; i < nPoints; i++) {
            p.lineTo(xPoints[i], yPoints[i]);
        }
        if (close) {
            p.closePath();
        }
        return p;
    }

    /**
     * Converts a rendered image to a {@code BufferedImage}.  This utility
     * method has come from a forum post by Jim Moore at:
     *

     *
     * http://www.jguru.com/faq/view.jsp?EID=114602
     *
     * @param img  the rendered image.
     *
     * @return A buffered image.
     */
    public static BufferedImage convertRenderedImage(RenderedImage img) {
        if (img instanceof BufferedImage) {
            return (BufferedImage) img;
        }
        ColorModel cm = img.getColorModel();
        int width = img.getWidth();
        int height = img.getHeight();
        WritableRaster raster = cm.createCompatibleWritableRaster(width, height);
        boolean isAlphaPremultiplied = cm.isAlphaPremultiplied();
        Hashtable properties = new Hashtable();
        String[] keys = img.getPropertyNames();
        if (keys != null) {
            for (int i = 0; i < keys.length; i++) {
                properties.put(keys[i], img.getProperty(keys[i]));
            }
        }
        BufferedImage result = new BufferedImage(cm, raster,
                isAlphaPremultiplied, properties);
        img.copyData(raster);
        return result;
    }
}


/* ===================================================
 * JFreeSVG : an SVG library for the Java(tm) platform
 * ===================================================
 *
 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
 *
 * Project Info:  http://www.jfree.org/jfreesvg/index.html
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.]
 *
 * If you do not wish to be bound by the terms of the GPL, an alternative
 * commercial license can be purchased.  For details, please see visit the
 * JFreeSVG home page:
 *
 * http://www.jfree.org/jfreesvg
 *
 */

package org.jfree.graphics2d.svg;

import java.awt.AWTException;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.ImageCapabilities;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DirectColorModel;
import java.awt.image.VolatileImage;
import java.awt.image.WritableRaster;

/**
 * A graphics configuration for the {@link SVGGraphics2D} class.
 */
public class SVGGraphicsConfiguration extends GraphicsConfiguration {

    private GraphicsDevice device;

    private int width, height;

    /**
     * Creates a new instance.
     *
     * @param width  the width of the bounds.
     * @param height  the height of the bounds.
     */
    public SVGGraphicsConfiguration(int width, int height) {
      super();
      this.width = width;
      this.height = height;
    }

    /**
     * Returns the graphics device that this configuration is associated with.
     *
     * @return The graphics device (never {@code null}).
     */
    @Override
    public GraphicsDevice getDevice() {
        if (this.device == null) {
            this.device = new SVGGraphicsDevice("JFreeSVG-GraphicsDevice",
                    this);
        }
        return this.device;
    }

    /**
     * Returns the color model for this configuration.
     *
     * @return The color model.
     */
    @Override
    public ColorModel getColorModel() {
        return getColorModel(Transparency.TRANSLUCENT);
    }

    /**
     * Returns the color model for the specified transparency type, or
     * {@code null}.
     *
     * @param transparency  the transparency type.
     *
     * @return A color model (possibly {@code null}).
     */
    @Override
    public ColorModel getColorModel(int transparency) {
        if (transparency == Transparency.TRANSLUCENT) {
            return ColorModel.getRGBdefault();
        } else if (transparency == Transparency.OPAQUE) {
            return new DirectColorModel(32, 0x00ff0000, 0x0000ff00, 0x000000ff);
        } else {
            return null;
        }
    }

    /**
     * Returns the default transform.
     *
     * @return The default transform.
     */
    @Override
    public AffineTransform getDefaultTransform() {
        return new AffineTransform();
    }

    /**
     * Returns the normalizing transform.
     *
     * @return The normalizing transform.
     */
    @Override
    public AffineTransform getNormalizingTransform() {
        return new AffineTransform();
    }

    /**
     * Returns the bounds for this configuration.
     *
     * @return The bounds.
     */
    @Override
    public Rectangle getBounds() {
        return new Rectangle(this.width, this.height);
    }

    /**
     * Creates a compatible image. This override is only here to provide
     * support for Java 6 because from Java 7 onwards the super class has a
     * non-abstract implementation for this method.
     *
     * @param width  the width.
     * @param height  the height.
     *
     * @return A compatible image.
     */
    @Override
    public BufferedImage createCompatibleImage(int width, int height) {
        ColorModel model = getColorModel();
        WritableRaster raster = model.createCompatibleWritableRaster(width,
                height);
        return new BufferedImage(model, raster, model.isAlphaPremultiplied(),
                null);
    }

    private BufferedImage img;
    private GraphicsConfiguration gc;

    /**
     * Returns a volatile image.  This method is a workaround for a
     * ClassCastException that occurs on MacOSX when exporting a Swing UI
     * that uses the Nimbus Look and Feel to SVG.
     *
     * @param width  the image width.
     * @param height  the image height.
     * @param caps  the image capabilities.
     * @param transparency  the transparency.
     *
     * @return The volatile image.
     *
     * @throws AWTException if there is a problem creating the image.
     */
    @Override
    public VolatileImage createCompatibleVolatileImage(int width, int height,
            ImageCapabilities caps, int transparency) throws AWTException {
        if (img == null) {
            img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
            gc = img.createGraphics().getDeviceConfiguration();
        }
        return gc.createCompatibleVolatileImage(width, height, caps,
                transparency);
    }

}


/* ===================================================
 * JFreeSVG : an SVG library for the Java(tm) platform
 * ===================================================
 *
 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
 *
 * Project Info:  http://www.jfree.org/jfreesvg/index.html
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.]
 *
 * If you do not wish to be bound by the terms of the GPL, an alternative
 * commercial license can be purchased.  For details, please see visit the
 * JFreeSVG home page:
 *
 * http://www.jfree.org/jfreesvg
 *
 */

package org.jfree.graphics2d;

/**
 * A utility class that performs checks for method argument validity.
 */
public class Args {

    private Args() {
        // no need to instantiate this
    }

    /**
     * Checks that an argument is non-{@code null} and throws an
     * {@code IllegalArgumentException} otherwise.
     *
     * @param obj  the object to check for {@code null}.
     * @param ref  the text name for the parameter (to include in the exception
     *     message).
     */
    public static void nullNotPermitted(Object obj, String ref) {
        if (obj == null) {
            throw new IllegalArgumentException("Null '" + ref + "' argument.");
        }
    }

    /**
     * Checks an array to ensure it has the correct length and throws an
     * {@code IllegalArgumentException} if it does not.
     *
     * @param length  the required length.
     * @param array  the array to check.
     * @param ref  the text name of the array parameter (to include in the
     *     exception message).
     */
    public static void arrayMustHaveLength(int length, boolean[] array,
            String ref) {
        nullNotPermitted(array, "array");
        if (array.length != length) {
            throw new IllegalArgumentException("Array '" + ref
                    + "' requires length " + length);
        }
    }

    /**
     * Checks an array to ensure it has the correct length and throws an
     * {@code IllegalArgumentException} if it does not.
     *
     * @param length  the required length.
     * @param array  the array to check ({@code null} not permitted).
     * @param ref  the text name of the array parameter (to include in the
     *     exception message).
     */
    public static void arrayMustHaveLength(int length, double[] array,
            String ref) {
        nullNotPermitted(array, "array");
        if (array.length != length) {
            throw new IllegalArgumentException("Array '" + ref
                    + "' requires length " + length);
        }
    }
}


/* ===================================================
 * JFreeSVG : an SVG library for the Java(tm) platform
 * ===================================================
 *
 * (C)opyright 2013-2019, by Object Refinery Limited.  All rights reserved.
 *
 * Project Info:  http://www.jfree.org/jfreesvg/index.html
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.]
 *
 * If you do not wish to be bound by the terms of the GPL, an alternative
 * commercial license can be purchased.  For details, please see visit the
 * JFreeSVG home page:
 *
 * http://www.jfree.org/jfreesvg
 *
 */

package org.jfree.graphics2d.svg;

/**
 * Represents a view box in SVG.
 *
 * @since 3.2
 */
public class ViewBox {

    private final int minX;

    private final int minY;

    private final int width;

    private final int height;

    /**
     * Creates a new instance with the specified dimensions.
     *
     * @param minX  the x coordinate.
     * @param minY  the y coordinate.
     * @param width  the width.
     * @param height  the height.
     */
    public ViewBox(int minX, int minY, int width, int height) {
        this.minX = minX;
        this.minY = minY;
        this.width = width;
        this.height = height;
    }

    /**
     * Returns a string containing the view box coordinates and dimensions.
     *
     * @return A string containing the view box coordinates and dimensions.
     */
    public String valueStr() {
        return new StringBuilder().append(this.minX).append(" ")
                .append(this.minY).append(" ").append(this.width).append(" ")
                .append(this.height).toString();
    }

}


/* ===================================================
 * JFreeSVG : an SVG library for the Java(tm) platform
 * ===================================================
 *
 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
 *
 * Project Info:  http://www.jfree.org/jfreesvg/index.html
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.]
 *
 * If you do not wish to be bound by the terms of the GPL, an alternative
 * commercial license can be purchased.  For details, please see visit the
 * JFreeSVG home page:
 *
 * http://www.jfree.org/jfreesvg
 *
 */

package org.jfree.graphics2d.svg;

import java.awt.Font;
import java.util.HashMap;
import java.util.Map;
import org.jfree.graphics2d.Args;

/**
 * A default implementation of the {@link FontMapper} interface.  This
 * implementation will map the Java logical fonts to equivalent SVG generic
 * fonts.  You can add your own mappings if you need to.
 *
 * @since 1.5
 */
public class StandardFontMapper implements FontMapper {

    /** Storage for the alternates. */
    private Map<String, String> alternates;

    /**
     * Creates a new instance with mappings for the Java logical fonts.
     */
    public StandardFontMapper() {
        this.alternates = new HashMap<String, String>();
        this.alternates.put(Font.DIALOG, "sans-serif");
        this.alternates.put(Font.DIALOG_INPUT, "monospace");
        this.alternates.put(Font.SANS_SERIF, "sans-serif");
        this.alternates.put(Font.SERIF, "serif");
        this.alternates.put(Font.MONOSPACED, "monospace");
    }

    /**
     * Returns the mapped (alternate) font family name.
     *
     * @param family  the font family ({@code null} not permitted).
     *
     * @return The alternate font family name (possibly {@code null}).
     */
    public String get(String family) {
        Args.nullNotPermitted(family, "family");
        return this.alternates.get(family);
    }

    /**
     * Adds a font family mapping (if the specified alternate is
     * {@code null} it has the effect of clearing any existing mapping).
     *
     * @param family  the font family name ({@code null} not permitted).
     * @param alternate  the alternate ({@code null} permitted).
     */
    public void put(String family, String alternate) {
        Args.nullNotPermitted(family, "family");
        this.alternates.put(family, alternate);
    }

    /**
     * Maps the specified font family name to an alternative, or else returns
     * the same family name.
     *
     * @param family  the font family name ({@code null} not permitted).
     *
     * @return The same font family name or an alternative (never {@code null}).
     */
    @Override
    public String mapFont(String family) {
        Args.nullNotPermitted(family, "family");
        String alternate = this.alternates.get(family);
        if (alternate != null) {
            return alternate;
        }
        return family;
    }

}