package de.escalon.hypermedia.hydra.serialize;

import de.escalon.hypermedia.AnnotationUtils;
import de.escalon.hypermedia.hydra.mapping.*;
import org.apache.commons.lang3.text.WordUtils;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

import static de.escalon.hypermedia.AnnotationUtils.findAnnotation;

/**
 * Provides LdContext information. Created by Dietrich on 05.04.2015.
 */
public class LdContextFactory {

    public static final String HTTP_SCHEMA_ORG = "http://schema.org/";
    private ProxyUnwrapper proxyUnwrapper;

    /**
     * Gets vocab for given bean.
     *
     * @param bean
     *         to inspect for vocab
     * @param mixInClass
     *         for bean which might define a vocab or has a context provider
     * @return explicitly defined vocab or http://schema.org
     */
    public String getVocab(MixinSource mixinSource, Object bean, Class<?> mixInClass) {
        if (proxyUnwrapper != null) {
            bean = proxyUnwrapper.unwrapProxy(bean);
        }

        // determine vocab in context
        String classVocab = bean == null ? null : vocabFromClassOrPackage(bean.getClass());

        final Vocab mixinVocab = findAnnotation(mixInClass, Vocab.class);

        Object nestedContextProviderFromMixin = getNestedContextProviderFromMixin(mixinSource, bean, mixInClass);
        String contextProviderVocab = null;
        if (nestedContextProviderFromMixin != null) {
            contextProviderVocab = getVocab(mixinSource, nestedContextProviderFromMixin, null);
        }

        String vocab;
        if (mixinVocab != null) {
            vocab = mixinVocab.value(); // wins over class
        } else if (classVocab != null) {
            vocab = classVocab; // wins over context provider
        } else if (contextProviderVocab != null) {
            vocab = contextProviderVocab; // wins over last resort
        } else {
            vocab = HTTP_SCHEMA_ORG;
        }
        return vocab;
    }

    public Map<String, Object> getTerms(MixinSource mixinSource, Object bean, Class<?> mixInClass) {

        try {

            if (proxyUnwrapper != null) {
                bean = proxyUnwrapper.unwrapProxy(bean);
            }

            Map<String, Object> termsMap = new LinkedHashMap<String, Object>();
            if (bean != null) {
                final Class<?> beanClass = bean.getClass();
                termsMap.putAll(termsFromClass(beanClass));
                Map<String, Object> mixinTermsMap = getAnnotatedTerms(mixInClass, beanClass
                        .getName());

                // mixin terms override class terms
                termsMap.putAll(mixinTermsMap);

                Object nestedContextProviderFromMixin = getNestedContextProviderFromMixin(mixinSource, bean,
                        mixInClass);

                if (nestedContextProviderFromMixin != null) {
                    termsMap.putAll(getTerms(mixinSource, nestedContextProviderFromMixin, null));
                }

                final Field[] fields = beanClass
                        .getDeclaredFields();
                for (Field field : fields) {
                    if (Modifier.isPublic(field.getModifiers())) {
                        final Expose expose = field.getAnnotation(Expose.class);
                        if (Enum.class.isAssignableFrom(field.getType())) {
                            addEnumTerms(termsMap, expose, field.getName(), (Enum) field.get(bean));
                        } else {
                            if (expose != null) {
                                termsMap.put(field.getName(), expose.value());
                            }
                        }
                    }
                }

                final BeanInfo beanInfo = Introspector.getBeanInfo(beanClass);
                final PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
                for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
                    final Method method = propertyDescriptor.getReadMethod();
                    if (method != null) {
                        final Expose expose = method.getAnnotation(Expose.class);
                        if (Enum.class.isAssignableFrom(method.getReturnType())) {
                            addEnumTerms(termsMap, expose, propertyDescriptor.getName(), (Enum) method.invoke(bean));
                        } else if ( returnsEnumCollection( method ) ) {
							Collection<? extends Enum> coll = ( Collection<? extends Enum> ) method.invoke( bean );
							for ( Enum item : coll ) {
								addEnumTerms(termsMap, expose, propertyDescriptor.getName(), item);
							}
                        } else {
                            if (expose != null) {
                                termsMap.put(propertyDescriptor.getName(), expose.value());
                            }
                        }
                    }
                }
            }
            return termsMap;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

	private boolean returnsEnumCollection( Method method ) {
		if ( Collection.class.isAssignableFrom( method.getReturnType() ) ) {
			Type t = method.getGenericReturnType();
			if ( t instanceof ParameterizedType ) {
				ParameterizedType pt = (ParameterizedType) t;
				if ( pt.getActualTypeArguments().length == 1 ) {
					Type arg = pt.getActualTypeArguments()[0];
					return Class.class.isInstance( arg ) && Enum.class.isAssignableFrom( (Class) arg );
				}
			}
		}
		return false;
    }

	/**
     * Gets explicitly defined terms, e.g. on package, class or mixin.
     *
     * @param annotatedElement
     *         to find terms
     * @param name
     *         of annotated element, i.e. class name or package name
     * @return terms
     */
    private Map<String, Object> getAnnotatedTerms(AnnotatedElement annotatedElement, String name) {
        final Terms annotatedTerms = findAnnotation(annotatedElement, Terms.class);
        final Term annotatedTerm = findAnnotation(annotatedElement, Term.class);

        if (annotatedTerms != null && annotatedTerm != null) {
            throw new IllegalStateException("found both @Terms and @Term in " + name + ", use either one or the other");
        }
        Map<String, Object> annotatedTermsMap = new LinkedHashMap<String, Object>();
        if (annotatedTerms != null) {
            final Term[] terms = annotatedTerms.value();
            for (Term term : terms) {
                collectTerms(name, annotatedTermsMap, term);
            }
        } else if (annotatedTerm != null) { // only one term
            collectTerms(name, annotatedTermsMap, annotatedTerm);
        }
        return annotatedTermsMap;
    }

    private void collectTerms(String name, Map<String, Object> annotatedTermsMap, Term term) {
        final String define = term.define();
        final String as = term.as();
        final boolean reverse = term.reverse();
        if (annotatedTermsMap.containsKey(as)) {
            throw new IllegalStateException("duplicate definition of term '" + define + "' in " + name);
        }
        if (!reverse) {
            annotatedTermsMap.put(define, as);
        } else {
            Map<String, String> reverseTerm = new LinkedHashMap<String, String>();
            reverseTerm.put(JsonLdKeywords.AT_REVERSE, as);
            annotatedTermsMap.put(define, reverseTerm);
        }
    }

    private Object getNestedContextProviderFromMixin(MixinSource mixinSource, Object bean, Class<?> mixinClass) {
        // TODO does not consider Collection<Resource> or Collection<PersistentEntityResource> to find mixin of
        // object wrapped in resource
        // TODO does not consider package of object wrapped in resource
        // TODO: we do not know Resources here
        if (mixinClass == null) {
            return null;
        }
        try {
            Method mixinContextProvider = getContextProvider(mixinClass);
            if (mixinContextProvider == null) {
                return null;
            }
            Class<?> beanClass = bean.getClass();
            Object contextual = beanClass.getMethod(mixinContextProvider.getName())
                    .invoke(bean);
            Object ret = null;
            if (contextual instanceof Collection) {
                Collection collection = (Collection) contextual;
                if (!collection.isEmpty()) {
                    Object item = collection.iterator()
                            .next();
                    final Class<?> mixInClass = mixinSource.findMixInClassFor(item.getClass());
                    if (mixInClass == null) {
                        ret = item;
                    } else {
                        ret = getNestedContextProviderFromMixin(mixinSource, item, mixInClass);
                    }
                }
            } else if (contextual instanceof Map) {
                Map map = (Map) contextual;
                if (!map.isEmpty()) {
                    Object item = map.values()
                            .iterator()
                            .next();
                    final Class<?> mixInClass = mixinSource.findMixInClassFor(item.getClass());
                    if (mixInClass == null) {
                        ret = item;
                    } else {
                        ret = getNestedContextProviderFromMixin(mixinSource, item, mixInClass);
                    }
                }
            } else {
                ret = contextual;
            }
            return ret;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private Method getContextProvider(Class<?> beanClass) {
        Class<? extends Annotation> annotation = ContextProvider.class;
        Method contextProvider = AnnotationUtils.getAnnotatedMethod(beanClass, annotation);
        if (contextProvider != null && contextProvider.getParameterTypes().length > 0) {
            throw new IllegalStateException("the context provider method " + contextProvider.getName() + " must not " +
                    "have arguments");
        }
        return contextProvider;
    }

    private void addEnumTerms(Map<String, Object> termsMap, Expose expose, String name,
                              Enum value) throws NoSuchFieldException {
        if (value != null) {
            Map<String, String> map = new LinkedHashMap<String, String>();
            if (expose != null) {
                map.put(JsonLdKeywords.AT_ID, expose.value());
            }
            map.put(JsonLdKeywords.AT_TYPE, JsonLdKeywords.AT_VOCAB);
            termsMap.put(name, map);
            final Expose enumValueExpose = findAnnotation(value.getClass()
                    .getField(value.name()), Expose.class);

            if (enumValueExpose != null) {
                termsMap.put(value.toString(), enumValueExpose.value());
            } else {
                // might use upperToCamelCase if nothing is exposed
                final String camelCaseEnumValue = WordUtils.capitalizeFully(value.toString(), new char[]{'_'})
                        .replaceAll("_", "");
                termsMap.put(value.toString(), camelCaseEnumValue);
            }
        }
    }

    public String vocabFromClassOrPackage(Class<?> clazz) {
        // vocab and terms of defining class: class and package
        final Vocab packageVocab = findAnnotation(clazz
                .getPackage(), Vocab.class);
        final Vocab classVocab = findAnnotation(clazz, Vocab.class);

        String vocab;
        if (classVocab != null) {
            vocab = classVocab.value(); // wins over package
        } else if (packageVocab != null) {
            vocab = packageVocab.value(); // wins over context provider
        } else {
            vocab = null;
        }
        return vocab;
    }

    public Map<String, Object> termsFromClass(Class<?> clazz) {
        Map<String, Object> termsMap = getAnnotatedTerms(clazz.getPackage(), clazz.getPackage()
                .getName());
        Map<String, Object> classTermsMap = getAnnotatedTerms(clazz, clazz.getName());

        // class terms override package terms
        termsMap.putAll(classTermsMap);
        return termsMap;
    }

    public void setProxyUnwrapper(ProxyUnwrapper proxyUnwrapper) {
        this.proxyUnwrapper = proxyUnwrapper;
    }
}


package de.escalon.hypermedia.sample.event;

import de.escalon.hypermedia.sample.beans.event.Event;
import de.escalon.hypermedia.sample.beans.event.Rating;
import de.escalon.hypermedia.sample.beans.event.Review;
import de.escalon.hypermedia.sample.model.event.CreativeWork;
import de.escalon.hypermedia.sample.model.event.EventModel;
import de.escalon.hypermedia.sample.model.event.EventStatusType;
import de.escalon.hypermedia.spring.AffordanceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.Resources;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

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

/**
 * Sample Event Controller. Created by dschulten on 28.12.2014.
 */
@Controller
@RequestMapping("/events")
public class EventController {

    @Autowired
    private EventBackend eventBackend;

    @Autowired
    private EventResourceAssembler assembler;

//    @RequestMapping(method = RequestMethod.GET)
//    @ResponseBody
//    public ResponseEntity<Resources<Event>> getEvents() {
//        List<Event> events = assembler.toResources(eventBackend.getEvents());
//        for (Event event : events) {
//            addAffordances(event);
//        }
//        Resources<Event> eventResources = new Resources<Event>(events);
//
//        eventResources.add(AffordanceBuilder.linkTo(AffordanceBuilder.methodOn(EventController.class).addEvent(null))
//                .withSelfRel());
//
//        return new ResponseEntity<Resources<Event>>(eventResources, HttpStatus.OK);
//    }

    @RequestMapping(method = RequestMethod.GET)
    @ResponseBody
    public ResponseEntity<Resources<Event>> findEvents(@RequestParam(required = false) String name) {
        List<Event> events = assembler.toResources(eventBackend.getEvents());
        List<Event> matches = new ArrayList<Event>();
        for (Event event : events) {
            if (name == null || event.workPerformed.getContent().name.equals(name)) {
                addAffordances(event);
                matches.add(event);
            }
        }
        Resources<Event> eventResources = new Resources<Event>(matches);

        eventResources.add(AffordanceBuilder.linkTo(AffordanceBuilder.methodOn(EventController.class)
                .addEvent(new Event(null, new CreativeWork(null), null, EventStatusType.EVENT_SCHEDULED)))
                .withSelfRel());

        eventResources.add(AffordanceBuilder.linkTo(AffordanceBuilder.methodOn(EventController.class)
                .findEvents(null))
                .withRel("hydra:search"));

        return new ResponseEntity<Resources<Event>>(eventResources, HttpStatus.OK);
    }

    @RequestMapping(value = "/{eventId}", method = RequestMethod.GET)
    public ResponseEntity<Event> getEvent(@PathVariable Integer eventId) {
        Event event = assembler.toResource(eventBackend.getEvent(eventId));

        addAffordances(event);

        return new ResponseEntity<Event>(event, HttpStatus.OK);
    }

    private void addAffordances(Event event) {
        event.add(AffordanceBuilder.linkTo(AffordanceBuilder.methodOn(this.getClass())
                .getEvent(event.id))
                .and(AffordanceBuilder.linkTo(AffordanceBuilder.methodOn(EventController.class)
                        .updateEvent(event.id, event)))
                .and(AffordanceBuilder.linkTo(AffordanceBuilder.methodOn(EventController.class)
                        .deleteEvent(event.id)))
                .withSelfRel());
        event.workPerformed.add(AffordanceBuilder.linkTo(AffordanceBuilder.methodOn(ReviewController.class)
                .addReview(event.id, new Review(null, new Rating(3))))
                .withRel("review"));
    }

    @RequestMapping(value = "/{eventId}", method = RequestMethod.PUT)
    public ResponseEntity<Void> updateEvent(@PathVariable int eventId, @RequestBody Event event) {
        eventBackend.updateEvent(eventId, event.getEventStatus());
        return new ResponseEntity<Void>(HttpStatus.NO_CONTENT);
    }

    @RequestMapping(value = "/{eventId}", method = RequestMethod.DELETE)
    public ResponseEntity<Void> deleteEvent(@PathVariable int eventId) {
        eventBackend.deleteEvent(eventId);
        return new ResponseEntity<Void>(HttpStatus.NO_CONTENT);
    }

    @RequestMapping(method = RequestMethod.POST)
    public ResponseEntity<Void> addEvent(@RequestBody Event event) {
        EventModel eventModel = new EventModel(-1, event.performer, event.workPerformed.getContent(), event.location,
                event.getEventStatus());
        int eventId = eventBackend.addEvent(eventModel);
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setLocation(AffordanceBuilder.linkTo(AffordanceBuilder.methodOn(this.getClass())
                .getEvent(eventId))
                .toUri());
        return new ResponseEntity<Void>(httpHeaders, HttpStatus.CREATED);
    }

}


/*
 * Copyright (c) 2014. Escalon System-Entwicklung, Dietrich Schulten
 *
 * 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 de.escalon.hypermedia.affordance;

import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.util.List;

/**
 * Represents components of a Uri Template with variables. Created by dschulten on 04.12.2014.
 */
public class PartialUriTemplateComponents {

    private String baseUri;
    private String queryHead;
    private String queryTail;
    private String fragmentIdentifier;
    private List<String> variableNames;

    /**
     * Represents components of a Uri Template with variables.
     *
     * @param fragmentIdentifier,
     *         beginning with #, may contain a fragment variable, may also be empty
     * @param baseUri
     *         may be relative or absolute, and may contain {xxx} or {/xxx} style variables
     * @param queryHead
     *         start of query containing expanded key-value pairs (no variables), beginning with ?, may be empty
     * @param queryTail
     *         comma-separated list of unexpanded query keys, may be empty
     * @param variableNames
     *         names of template variables
     */
    public PartialUriTemplateComponents(String baseUri, String queryHead, String queryTail, String
            fragmentIdentifier, List<String> variableNames) {
        Assert.notNull(baseUri);
        Assert.notNull(queryHead);
        Assert.notNull(queryTail);
        Assert.notNull(fragmentIdentifier);
        Assert.notNull(variableNames);
        this.baseUri = baseUri;
        this.queryHead = queryHead;
        this.queryTail = queryTail;
        this.fragmentIdentifier = fragmentIdentifier;
        this.variableNames = variableNames;
    }

    public List<String> getVariableNames() {
        return variableNames;
    }

    public boolean isBaseUriTemplated() {
        return baseUri.matches(".*\\{.+\\}.*");
    }

    public String getBaseUri() {
        return baseUri;
    }

    /**
     * Query head starting with ? continued by expanded query parameters, separated by &
     *
     * @return query head, may be empty
     */
    public String getQueryHead() {
        return queryHead;
    }

    /**
     * Query tail containing unexpanded query parameters as comma-separated list.
     *
     * @return query tail, may be empty
     */
    public String getQueryTail() {
        return queryTail;
    }

    /**
     * Query consisting of expanded parameters and unexpanded parameters.
     *
     * @return query, may be empty
     */
    public String getQuery() {
        StringBuilder query = new StringBuilder();
        if (queryTail.length() > 0) {
            if (queryHead.length() == 0) {
                query.append("{?")
                        .append(queryTail)
                        .append("}");
            } else if (queryHead.length() > 0) {
                query.append(queryHead)
                        .append("{&")
                        .append(queryTail)
                        .append("}");
            }
        } else {
            query.append(queryHead);
        }
        return query.toString();
    }

    public String getFragmentIdentifier() {
        return fragmentIdentifier;
    }

    /**
     * Concatenates all components to uri String.
     *
     * @return uri String
     */
    public String toString() {
        return baseUri + getQuery() + fragmentIdentifier;
    }

    public boolean hasVariables() {
        return !variableNames.isEmpty();
    }
}


package de.escalon.hypermedia.spring.xhtml;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import de.escalon.hypermedia.PropertyUtils;
import de.escalon.hypermedia.action.Input;
import de.escalon.hypermedia.action.Type;
import de.escalon.hypermedia.affordance.ActionDescriptor;
import de.escalon.hypermedia.affordance.ActionInputParameter;
import de.escalon.hypermedia.affordance.Affordance;
import de.escalon.hypermedia.affordance.DataType;
import de.escalon.hypermedia.spring.DefaultDocumentationProvider;
import de.escalon.hypermedia.spring.DocumentationProvider;
import de.escalon.hypermedia.spring.SpringActionInputParameter;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.Property;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.TemplateVariable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.RequestMethod;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.*;

import static de.escalon.hypermedia.spring.xhtml.XhtmlWriter.OptionalAttributes.attr;

/**
 * Created by Dietrich on 09.02.2015.
 */
public class XhtmlWriter extends Writer {

    private Writer writer;
    private List<String> stylesheets = Collections.emptyList();

    public static final String HTML_HEAD_START = "" + //
            //"<?xml version='1.0' encoding='UTF-8' ?>" + // formatter
            "<!DOCTYPE html>" + //
            "" + //
            "  " + //
            "    <meta charset=\"utf-8\"/>" + //
            "    <title>%s</title>";

    public static final String HTML_STYLESHEET = "" + //
            "    <link rel=\"stylesheet\" href=\"%s\"  />";

    public static final String HTML_HEAD_END = "" + //
            "  " + //
            "  " + //
            "
\n" + //
            "
";

    public static final String HTML_END = "" + //
            "
" +
            "
" +
            "  " + //
            "";

    private String methodParam = "_method";
    private DocumentationProvider documentationProvider = new DefaultDocumentationProvider();

    private String formControlClass = "form-control";
    private String formGroupClass = "form-group";
    private String controlLabelClass = "control-label";

    public XhtmlWriter(Writer writer) {
        this.writer = writer;
    }

    public void setMethodParam(String methodParam) {
        this.methodParam = methodParam;
    }

    public void beginHtml(String title) throws IOException {
        write(String.format(HTML_HEAD_START, title));
        for (String stylesheet : stylesheets) {
            write(String.format(HTML_STYLESHEET, stylesheet));
        }
        write(String.format(HTML_HEAD_END, title));
    }

    public void endHtml() throws IOException {
        write(HTML_END);
    }

    public void beginDiv() throws IOException {
        writer.write("
");
    }

    public void beginDiv(OptionalAttributes attributes) throws IOException {
        writer.write("<div ");
        writeAttributes(attributes);
        endTag();
    }

    public void endDiv() throws IOException {
        writer.write("
");
    }

    @Override
    public void write(char[] cbuf, int off, int len) throws IOException {
        writer.write(cbuf, off, len);
    }

    @Override
    public void flush() throws IOException {
        writer.flush();
    }

    @Override
    public void close() throws IOException {
        writer.close();
    }

    public void beginUnorderedList() throws IOException {
        writer.write("
");
    }

    public void endUnorderedList() throws IOException {
        writer.write("
");
    }

    public void beginListItem() throws IOException {
        writer.write("
");
    }

    public void endListItem() throws IOException {
        writer.write("
");
    }

    public void beginSpan() throws IOException {
        writer.write("");
    }

    public void endSpan() throws IOException {
        writer.write("");
    }

    public void beginDl() throws IOException {
        // TODO: make this configurable?
        writer.write("
");
    }

    public void endDl() throws IOException {
        writer.write("
");
    }

    public void beginDt() throws IOException {
        writer.write("
");
    }

    public void endDt() throws IOException {
        writer.write("
");
    }

    public void beginDd() throws IOException {
        writer.write("
");
    }

    public void endDd() throws IOException {
        writer.write("
");
    }

    public void writeSpan(Object value) throws IOException {
        beginSpan();
        writer.write(value.toString());
        endSpan();
    }

    public void writeDefinitionTerm(Object value) throws IOException {
        beginDt();
        writer.write(value.toString());
        endDt();
    }

    public void setStylesheets(List<String> stylesheets) {
        Assert.notNull(stylesheets);
        this.stylesheets = stylesheets;
    }

    public void setDocumentationProvider(DocumentationProvider documentationProvider) {
        this.documentationProvider = documentationProvider;
    }

    public static class OptionalAttributes {

        private Map<String, String> attributes = new LinkedHashMap<String, String>();

        @Override
        public String toString() {
            return attributes.toString();
        }

        /**
         * Creates OptionalAttributes with one optional attribute having name if value is not null.
         *
         * @param name
         *         of first attribute
         * @param value
         *         may be null
         * @return builder with one attribute, attr builder if value is null
         */
        public static OptionalAttributes attr(String name, String value) {
            Assert.isTrue(name != null && value != null || value == null);
            OptionalAttributes attributeBuilder = new OptionalAttributes();
            addAttributeIfValueNotNull(name, value, attributeBuilder);
            return attributeBuilder;
        }

        private static void addAttributeIfValueNotNull(String name, String value, OptionalAttributes attributeBuilder) {
            if (value != null) {
                attributeBuilder.attributes.put(name, value);
            }
        }

        public OptionalAttributes and(String name, String value) {
            addAttributeIfValueNotNull(name, value, this);
            return this;
        }

        public Map<String, String> build() {
            return attributes;
        }

        /**
         * Creates OptionalAttributes builder.
         *
         * @return builder
         */
        public static OptionalAttributes attr() {
            return attr(null, null);
        }
    }

    public void writeLinks(List<Link> links) throws IOException {
        for (Link link : links) {

            if (link instanceof Affordance) {
                Affordance affordance = (Affordance) link;
                List<ActionDescriptor> actionDescriptors = affordance.getActionDescriptors();
                if (actionDescriptors.isEmpty()) {
                    // treat like simple link
                    appendLinkWithoutActionDescriptor(affordance);
                } else {
                    if (affordance.isTemplated()) {
                        // TODO ensure that template expansion always takes place for base uri
                        if (!affordance.isBaseUriTemplated()) {
                            for (ActionDescriptor actionDescriptor : actionDescriptors) {
                                RequestMethod httpMethod = RequestMethod.valueOf(actionDescriptor.getHttpMethod());
                                // html does not allow templated action attr for forms, only render GET form
                                if (RequestMethod.GET == httpMethod) {
                                    // TODO: partial uritemplate query must become hidden field
                                    appendForm(affordance, actionDescriptor);
                                }
                                // TODO write human-readable description of additional methods?
                            }
                        }
                    } else {
                        for (ActionDescriptor actionDescriptor : actionDescriptors) {
                            // TODO write documentation about the supported action and maybe fields?
                            if ("GET".equals(actionDescriptor.getHttpMethod()) &&
                                    actionDescriptor.getRequestParamNames()
                                            .isEmpty()) {
                                beginDiv();
                                // GET without params is simple
                                writeAnchor(OptionalAttributes.attr("href", affordance.expand()
                                        .getHref())
                                        .and("rel", affordance.getRel()), affordance.getRel());
                                endDiv();
                            } else {
                                appendForm(affordance, actionDescriptor);
                            }
                        }
                    }
                }
            } else { // simple link, may be templated
                appendLinkWithoutActionDescriptor(link);
            }
        }
    }

    /**
     * Appends form and squashes non-GET or POST to POST. If required, adds _method field for handling by an
     * appropriate
     * filter such as Spring's HiddenHttpMethodFilter.
     *
     * @param affordance
     *         to make into a form
     * @param actionDescriptor
     *         describing the form action
     * @throws IOException
     * @see
     *
     * Spring
     * MVC HiddenHttpMethodFilter
     */
    private void appendForm(Affordance affordance, ActionDescriptor actionDescriptor) throws IOException {
        String formName = actionDescriptor.getActionName();
        RequestMethod httpMethod = RequestMethod.valueOf(actionDescriptor.getHttpMethod());

        // Link's expand method removes non-required variables from URL
        String actionUrl = affordance.expand()
                .getHref();
        beginForm(OptionalAttributes.attr("action", actionUrl)
                .and("method", getHtmlConformingHttpMethod(httpMethod))
                .and("name", formName));
        write("
");
        write("Form " + formName);
        write("
");

        writeHiddenHttpMethodField(httpMethod);
        // build the form
        if (actionDescriptor.hasRequestBody()) { // parameter bean
            ActionInputParameter requestBody = actionDescriptor.getRequestBody();
            Class<?> parameterType = requestBody.getParameterType();
            recurseBeanProperties(parameterType, actionDescriptor, requestBody, requestBody.getValue(), "");
        } else { // plain parameter list
            Collection<String> requestParams = actionDescriptor.getRequestParamNames();
            for (String requestParamName : requestParams) {
                ActionInputParameter actionInputParameter = actionDescriptor.getActionInputParameter(requestParamName);

                Object[] possibleValues = actionInputParameter.getPossibleValues(actionDescriptor);
                // TODO duplication with appendInputOrSelect
                if (possibleValues.length > 0) {
                    if (actionInputParameter.isArrayOrCollection()) {
                        appendSelectMulti(requestParamName, possibleValues, actionInputParameter);
                    } else {
                        appendSelectOne(requestParamName, possibleValues, actionInputParameter);
                    }
                } else {
                    if (actionInputParameter.isArrayOrCollection()) {
                        // have as many inputs as there are call values, list of 5 nulls gives you five input fields
                        // TODO support for free list input instead, code on demand?
                        Object[] callValues = actionInputParameter.getValues();
                        int items = callValues.length;
                        for (int i = 0; i < items; i++) {
                            Object value;
                            if (i < callValues.length) {
                                value = callValues[i];
                            } else {
                                value = null;
                            }
                            appendInput(requestParamName, actionInputParameter, value, actionInputParameter
									.isReadOnly(requestParamName)); // not readonly
                        }
                    } else {
                        String callValueFormatted = actionInputParameter.getValueFormatted();
                        appendInput(requestParamName, actionInputParameter, callValueFormatted, actionInputParameter
								.isReadOnly(requestParamName)); // not readonly
                    }
                }
            }
        }
        inputButton(Type.SUBMIT, capitalize(httpMethod.name()
                .toLowerCase()));
        endForm();
    }

    private void appendLinkWithoutActionDescriptor(Link link) throws IOException {
        if (link.isTemplated()) {
            // TODO ensure that template expansion takes place for base uri
            Link expanded = link.expand(); // remove query variables
            beginForm(OptionalAttributes.attr("action", expanded.getHref())
                    .and("method", "GET"));
            List<TemplateVariable> variables = link.getVariables();
            for (TemplateVariable variable : variables) {
                String variableName = variable.getName();
                String label = variable.hasDescription() ? variable.getDescription() : variableName;
                writeLabelWithDoc(label, variableName, null); // no documentation url
                input(variableName, Type.TEXT);
            }
        } else {
            String rel = link.getRel();
            String title = (rel != null ? rel : link.getHref());
            // TODO: write html <link> instead of anchor   here?
            writeAnchor(OptionalAttributes.attr("href", link.getHref())
                    .and("rel", link.getRel()), title);
        }
    }

    /**
     * Classic submit or reset button.
     *
     * @param type
     *         submit or reset
     * @param value
     *         caption on the button
     * @throws IOException
     */
    private void inputButton(Type type, String value) throws IOException {
        write("<input type=\"");
        write(type.toString());
        write("\" ");
        write("value");
        write("=");
        quote();
        write(value);
        quote();
        write("/>");
    }

    private void input(String fieldName, Type type, OptionalAttributes attributes) throws IOException {
        write("<input name=\"");
        write(fieldName);
        write("\" type=\"");
        write(type.toString());
        write("\" class=\"");
        write(formControlClass);
        write("\" ");
        writeAttributes(attributes);
        write("/>");
    }

    private void input(String fieldName, Type type) throws IOException {
        input(fieldName, type, OptionalAttributes.attr());
    }

//    private void beginLabel(String label) throws IOException {
//        beginLabel(label, attr());
//    }

//    private void beginLabel(String label, OptionalAttributes attributes) throws IOException {
//        beginLabel(attributes);
//        write(label);
//    }

    private void beginLabel(OptionalAttributes attributes) throws IOException {
        write("<label");
        writeAttributes(attributes);
        endTag();
    }

    private void endLabel() throws IOException {
        write("");
    }

    private void beginForm(OptionalAttributes attrs) throws IOException {
        write("<form class=\"well\" ");
        writeAttributes(attrs);
        write(">");
    }

    private void writeAttributes(OptionalAttributes attrs) throws IOException {
        Map<String, String> attributes = attrs.build();
        for (Map.Entry<String, String> entry : attributes.entrySet()) {
            write(" ");
            write(entry.getKey());
            write("=");
            quote();
            write(entry.getValue());
            quote();
        }
    }

    private void quote() throws IOException {
        write("\"");
    }

    private void endForm() throws IOException {
        write("</form>");
    }

    public void beginAnchor(OptionalAttributes attrs) throws IOException {
        write("<a ");
        writeAttributes(attrs);
        endTag();
    }

    public void endAnchor() throws IOException {
        write("");
    }

    public void writeBr() throws IOException {
        write("
");
    }

    private void writeAnchor(OptionalAttributes attrs, String value) throws IOException {
        beginAnchor(attrs);
        write(value);
        endAnchor();
    }

    public static String capitalize(String name) {
        if (name != null && name.length() != 0) {
            char[] chars = name.toCharArray();
            chars[0] = Character.toUpperCase(chars[0]);
            return new String(chars);
        } else {
            return name;
        }
    }

    private void writeHiddenHttpMethodField(RequestMethod httpMethod) throws IOException {
        switch (httpMethod) {
            case GET:
            case POST:
                break;
            default:
                input(methodParam, Type.HIDDEN, OptionalAttributes.attr("value", httpMethod.name()));
        }
    }

    private String getHtmlConformingHttpMethod(RequestMethod requestMethod) {
        String ret;
        switch (requestMethod) {
            case GET:
            case POST:
                ret = requestMethod.name();
                break;
            default:
                ret = RequestMethod.POST.name();
        }
        return ret;
    }

    /**
     * Renders input fields for bean properties of bean to add or update or patch.
     *
     * @param beanType
     *         to render
     * @param actionDescriptor
     *         which describes the method
     * @param actionInputParameter
     *         which requires the bean
     * @param currentCallValue
     *         sample call value
     * @throws IOException
     */
    private void recurseBeanProperties(Class<?> beanType, ActionDescriptor actionDescriptor, ActionInputParameter
            actionInputParameter, Object currentCallValue, String parentParamName) throws IOException {
        // TODO support Option provider by other method args?
        final BeanInfo beanInfo = getBeanInfo(beanType);
        final PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
        // TODO collection and map

        // TODO: do not add two inputs for setter and ctor

        // TODO almost duplicate of HtmlResourceMessageConverter.recursivelyCreateObject
        if (RequestMethod.POST == RequestMethod.valueOf(actionDescriptor.getHttpMethod())) {
            try {
                Constructor[] constructors = beanType.getConstructors();
                // find default ctor
                Constructor constructor = PropertyUtils.findDefaultCtor(constructors);
                // find ctor with JsonCreator ann
                if (constructor == null) {
                    constructor = PropertyUtils.findJsonCreator(constructors, JsonCreator.class);
                }
                Assert.notNull(constructor, "no default constructor or JsonCreator found for type " + beanType
                        .getName());
                int parameterCount = constructor.getParameterTypes().length;
                if (parameterCount > 0) {
                    Annotation[][] annotationsOnParameters = constructor.getParameterAnnotations();

                    Class[] parameters = constructor.getParameterTypes();
                    int paramIndex = 0;
                    for (Annotation[] annotationsOnParameter : annotationsOnParameters) {
                        for (Annotation annotation : annotationsOnParameter) {
                            if (JsonProperty.class == annotation.annotationType()) {
                                JsonProperty jsonProperty = (JsonProperty) annotation;
                                // TODO use required attribute of JsonProperty
                                String paramName = jsonProperty.value();
                                Class parameterType = parameters[paramIndex];

                                // TODO duplicate below for PropertyDescriptors and in appendForm
                                if (DataType.isSingleValueType(parameterType)) {

                                    if (actionInputParameter.isIncluded(paramName)) {

                                        Object propertyValue = PropertyUtils.getPropertyOrFieldValue(currentCallValue,
                                                paramName);

                                        ActionInputParameter constructorParamInputParameter = new
												SpringActionInputParameter
                                                (new MethodParameter(constructor, paramIndex), propertyValue);

                                        final Object[] possibleValues =
                                                actionInputParameter.getPossibleValues(
                                                        constructor, paramIndex, actionDescriptor);

                                        appendInputOrSelect(actionInputParameter, parentParamName + paramName,
                                                constructorParamInputParameter, possibleValues);
                                    }
                                } else if (DataType.isArrayOrCollection(parameterType)) {
                                    Object[] callValues = actionInputParameter.getValues();
                                    int items = callValues.length;
                                    for (int i = 0; i < items; i++) {
                                        Object value;
                                        if (i < callValues.length) {
                                            value = callValues[i];
                                        } else {
                                            value = null;
                                        }
                                        recurseBeanProperties(actionInputParameter.getParameterType(),
                                                actionDescriptor, actionInputParameter, value, parentParamName);
                                    }
                                } else {
                                    beginDiv();
                                    write(paramName + ":");
                                    Object propertyValue = PropertyUtils.getPropertyOrFieldValue(currentCallValue,
                                            paramName);
                                    recurseBeanProperties(parameterType, actionDescriptor, actionInputParameter,
                                            propertyValue, paramName + ".");
                                    endDiv();
                                }
                                paramIndex++; // increase for each @JsonProperty
                            }
                        }
                    }
                    Assert.isTrue(parameters.length == paramIndex,
                            "not all constructor arguments of @JsonCreator " + constructor.getName() +
                                    " are annotated with @JsonProperty");
                }
            } catch (Exception e) {
                throw new RuntimeException("Failed to write input fields for constructor", e);
            }
        } else { // non-POST

            // TODO non-writable properties and public fields: make sure the inputs are part of a form
            // write input field for every setter
            for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
                final Method writeMethod = propertyDescriptor.getWriteMethod();
                if (writeMethod == null) {
                    continue;
                }
                final Class<?> propertyType = propertyDescriptor.getPropertyType();

                String propertyName = propertyDescriptor.getName();

                if (DataType.isSingleValueType(propertyType)) {

                    final Property property = new Property(beanType, propertyDescriptor.getReadMethod(),
                            propertyDescriptor.getWriteMethod(), propertyDescriptor.getName());

                    Object propertyValue = PropertyUtils.getPropertyOrFieldValue(currentCallValue, propertyName);
                    MethodParameter methodParameter = new MethodParameter(propertyDescriptor.getWriteMethod(), 0);
                    ActionInputParameter propertySetterInputParameter = new SpringActionInputParameter(methodParameter,
                            propertyValue);
                    final Object[] possibleValues = actionInputParameter.getPossibleValues(propertyDescriptor
                                    .getWriteMethod(), 0,
                            actionDescriptor);
                    appendInputOrSelect(actionInputParameter, propertyName, propertySetterInputParameter,
							possibleValues);
                } else if (actionInputParameter.isArrayOrCollection()) {
                    Object[] callValues = actionInputParameter.getValues();
                    int items = callValues.length;
                    for (int i = 0; i < items; i++) {
                        Object value;
                        if (i < callValues.length) {
                            value = callValues[i];
                        } else {
                            value = null;
                        }
                        recurseBeanProperties(actionInputParameter.getParameterType(), actionDescriptor,
                                actionInputParameter, value, parentParamName);
                    }
                } else {
                    beginDiv();
                    write(propertyName + ":");
                    Object propertyValue = PropertyUtils.getPropertyValue(currentCallValue, propertyDescriptor);
                    recurseBeanProperties(propertyType, actionDescriptor, actionInputParameter, propertyValue,
							parentParamName);
                    endDiv();
                }
            }
        }
    }

    /**
     * Appends simple input or select, depending on availability of possible values.
     *
     * @param parentInputParameter
     *         the parent during bean recursion
     * @param paramName
     *         of the child input parameter
     * @param childInputParameter
     *         the current input to be rendered
     * @param possibleValues
     *         suitable for childInputParameter
     * @throws IOException
     */
    private void appendInputOrSelect(ActionInputParameter parentInputParameter, String paramName, ActionInputParameter
            childInputParameter, Object[] possibleValues) throws IOException {
        if (possibleValues.length > 0) {
            if (childInputParameter.isArrayOrCollection()) {
                // TODO multiple formatted callvalues
                appendSelectMulti(paramName, possibleValues,
                        childInputParameter);
            } else {
                appendSelectOne(paramName, possibleValues,
                        childInputParameter);
            }
        } else {
            appendInput(paramName, childInputParameter,
                    childInputParameter.getValue(),
                    parentInputParameter.isReadOnly(paramName));
        }
    }

    private BeanInfo getBeanInfo(Class<?> beanType) {
        try {
            return Introspector.getBeanInfo(beanType);
        } catch (IntrospectionException e) {
            throw new RuntimeException(e);
        }
    }

    private void appendInput(String requestParamName, ActionInputParameter actionInputParameter, Object value, boolean
            readOnly) throws
            IOException {
        if (actionInputParameter.isRequestBody()) { // recurseBeanProperties does that
            throw new IllegalArgumentException("cannot append input field for requestBody");
        }
        String fieldLabel = requestParamName;
        Type htmlInputFieldType = actionInputParameter.getHtmlInputFieldType();
        Assert.notNull(htmlInputFieldType);
        String val = value == null ? "" : value.toString();
        beginDiv(OptionalAttributes.attr("class", formGroupClass));
        if (Type.HIDDEN.equals(htmlInputFieldType)) {
            input(requestParamName, htmlInputFieldType, OptionalAttributes.attr("value", val));
        } else {
            String documentationUrl = documentationProvider.getDocumentationUrl(actionInputParameter, value);
            // TODO consider @Input-include/exclude/hidden here
            OptionalAttributes attrs = OptionalAttributes.attr("value", val);
            if (readOnly) {
                attrs.and(Input.READONLY, Input.READONLY);
            }
            writeLabelWithDoc(fieldLabel, requestParamName, documentationUrl);
            if (actionInputParameter.hasInputConstraints()) {
                for (Map.Entry<String, Object> inputConstraint : actionInputParameter.getInputConstraints()
                        .entrySet()) {
                    attrs.and(inputConstraint.getKey(), inputConstraint.getValue()
                            .toString());
                }
            }
            input(requestParamName, htmlInputFieldType, attrs);
        }
        endDiv();
    }

    private void writeLabelWithDoc(String label, String fieldName, String documentationUrl) throws IOException {
        beginLabel(OptionalAttributes.attr("for", fieldName)
                .and("class", controlLabelClass));
        if (documentationUrl == null) {
            write(label);
        } else {
            beginAnchor(XhtmlWriter.OptionalAttributes.attr("href", documentationUrl)
                    .and("title", documentationUrl));
            write(label);
            endAnchor();
        }
        endLabel();
    }

    private void appendSelectOne(String requestParamName, Object[] possibleValues, ActionInputParameter
            actionInputParameter)
            throws IOException {
        beginDiv(OptionalAttributes.attr("class", formGroupClass));
        Object callValue = actionInputParameter.getValue();
        String documentationUrl = documentationProvider.getDocumentationUrl(actionInputParameter, callValue);
        writeLabelWithDoc(requestParamName, requestParamName, documentationUrl);
        beginSelect(requestParamName, requestParamName, possibleValues.length,
                OptionalAttributes.attr("class", formControlClass));
        for (Object possibleValue : possibleValues) {
            if (possibleValue.equals(callValue)) {
                option(possibleValue.toString(), attr("selected", "selected"));
            } else {
                option(possibleValue.toString());
            }
        }
        endSelect();

        endDiv();
    }

    private void appendSelectMulti(String requestParamName, Object[] possibleValues, ActionInputParameter
            actionInputParameter) throws IOException {
        beginDiv(OptionalAttributes.attr("class", formGroupClass));
        Object[] actualValues = actionInputParameter.getValues();
        final Object aCallValue;
        if (actualValues.length > 0) {
            aCallValue = actualValues[0];
        } else {
            aCallValue = null;
        }
        String documentationUrl = documentationProvider.getDocumentationUrl(actionInputParameter, aCallValue);
        writeLabelWithDoc(requestParamName, requestParamName, documentationUrl);
        beginSelect(requestParamName, requestParamName, possibleValues.length,
                OptionalAttributes.attr("multiple", "multiple")
                        .and("class", formControlClass));
        for (Object possibleValue : possibleValues) {
            if (ObjectUtils.containsElement(actualValues, possibleValue)) {
                option(possibleValue.toString(), attr("selected", "selected"));
            } else {
                option(possibleValue.toString());
            }
        }
        endForm();
        endDiv();
    }

    private void option(String option) throws IOException {
        option(option, attr());
    }

    private void option(String option, OptionalAttributes attr) throws IOException {
        // <option selected='selected'>%s</option>
        beginTag("option");
        writeAttributes(attr);
        endTag();
        write(option);
        write("</option>");
    }

    private void beginTag(String tag) throws IOException {
        write("<");
        write(tag);
    }

    private void endTag() throws IOException {
        write(">");
    }

    private void beginSelect(String name, String id, int size) throws IOException {
        beginSelect(name, id, size, attr());
    }

    private void beginSelect(String name, String id, int size, OptionalAttributes attrs) throws IOException {
        beginTag("select");
        write(" name=");
        quote(name);
        write(" id=");
        quote(id);
        //write(" size=");
        //quote(Integer.toString(size));
        writeAttributes(attrs);
        endTag();
    }

    private void endSelect() throws IOException {
        write("</select>");
    }

    private void quote(String s) throws IOException {
        quote();
        write(s);
        quote();
    }
}


/*
 * Copyright (c) 2014. Escalon System-Entwicklung, Dietrich Schulten
 *
 * 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 de.escalon.hypermedia.spring;

import de.escalon.hypermedia.affordance.*;
import org.springframework.hateoas.Identifiable;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkBuilder;
import org.springframework.hateoas.core.DummyInvocationUtils;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriComponentsBuilder;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import static org.springframework.util.StringUtils.hasText;

/**
 * Builder for hypermedia affordances, usable as rfc-5988 web links and optionally holding information about request
 * body requirements. Created by dschulten on 07.09.2014.
 */
public class AffordanceBuilder implements LinkBuilder {

    private static final AffordanceBuilderFactory FACTORY = new AffordanceBuilderFactory();

    private PartialUriTemplateComponents partialUriTemplateComponents;
    private List<ActionDescriptor> actionDescriptors = new ArrayList<ActionDescriptor>();

    private MultiValueMap<String, String> linkParams = new LinkedMultiValueMap<String, String>();
    private List<String> rels = new ArrayList<String>();
    private List<String> reverseRels = new ArrayList<String>();
    private TypedResource collectionHolder;

    /**
     * Creates a new {@link AffordanceBuilder} with a base of the mapping annotated to the given controller class.
     *
     * @param controller
     *         the class to discover the annotation on, must not be {@literal null}.
     * @return builder
     */
    public static AffordanceBuilder linkTo(Class<?> controller) {
        return linkTo(controller, new Object[0]);
    }

    /**
     * Creates a new {@link AffordanceBuilder} with a base of the mapping annotated to the given controller class. The
     * additional parameters are used to fill up potentially available path variables in the class scope request
     * mapping.
     *
     * @param controller
     *         the class to discover the annotation on, must not be {@literal null}.
     * @param parameters
     *         additional parameters to bind to the URI template declared in the annotation, must not be {@literal
     *         null}.
     * @return builder
     */
    public static AffordanceBuilder linkTo(Class<?> controller, Object... parameters) {
        return FACTORY.linkTo(controller, parameters);
    }

    /**
     * @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(Method, Object...)
     */
    public static AffordanceBuilder linkTo(Method method, Object... parameters) {
        return linkTo(method.getDeclaringClass(), method, parameters);
    }

    /**
     * Creates a new {@link AffordanceBuilder} with a base of the mapping annotated to the given controller class. The
     * additional parameters are used to fill up potentially available path variables in the class scop request
     * mapping.
     *
     * @param controller
     *         the class to discover the annotation on, must not be {@literal null}.
     * @param parameters
     *         additional parameters to bind to the URI template declared in the annotation, must not be {@literal
     *         null}.
     * @return
     */
    public static AffordanceBuilder linkTo(Class<?> controller, Map<String, ?> parameters) {
        return FACTORY.linkTo(controller, parameters);
    }

    /**
     * @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(Class, Method, Object...)
     */
    public static AffordanceBuilder linkTo(Class<?> controller, Method method, Object... parameters) {
        return FACTORY.linkTo(controller, method, parameters);
    }

    /**
     * Creates a {@link AffordanceBuilder} pointing to a controller method. Hand in a dummy method invocation result
     * you
     * can create via {@link #methodOn(Class, Object...)} or {@link DummyInvocationUtils#methodOn(Class, Object...)}.
     *

     * @RequestMapping("/customers")
     * class CustomerController {
     *   @RequestMapping("/{id}/addresses")
     *   HttpEntity<Addresses> showAddresses(@PathVariable Long id) { � }
     * }
     * Link link = linkTo(methodOn(CustomerController.class).showAddresses(2L)).withRel("addresses");
     *

     * The resulting {@link Link} instance will point to {@code /customers/2/addresses} and have a rel of {@code
     * addresses}. For more details on the method invocation constraints, see {@link
     * DummyInvocationUtils#methodOn(Class, Object...)}.
     *
     * @param methodInvocation
     * @return
     */
    public static AffordanceBuilder linkTo(Object methodInvocation) {
        return FACTORY.linkTo(methodInvocation);
    }

    /**
     * Creates a new {@link AffordanceBuilder} pointing to this server, but without ActionDescriptor.
     */
    AffordanceBuilder() {
        this(new PartialUriTemplate(getBuilder().build()
                        .toString()).expand(Collections.<String, Object>emptyMap()),
                Collections.<ActionDescriptor>emptyList());
    }

    /**
     * Creates a new {@link AffordanceBuilder} using the given {@link ActionDescriptor}.
     *
     * @param partialUriTemplateComponents
     *         must not be {@literal null}
     * @param actionDescriptors
     *         must not be {@literal null}
     */
    public AffordanceBuilder(PartialUriTemplateComponents partialUriTemplateComponents, List<ActionDescriptor>
            actionDescriptors) {

        Assert.notNull(partialUriTemplateComponents);
        Assert.notNull(actionDescriptors);

        this.partialUriTemplateComponents = partialUriTemplateComponents;

        for (ActionDescriptor actionDescriptor : actionDescriptors) {
            this.actionDescriptors.add(actionDescriptor);
        }
    }

    public static <T> T methodOn(Class<T> clazz, Object... parameters) {
        return DummyInvocationUtils.methodOn(clazz, parameters);
    }

    /**
     * Builds affordance with one or multiple rels which must have been defined previously using {@link #rel(String)} or
     * {@link #reverseRel(String, String)}.
The motivation for multiple rels is this statement in the web linking
     * rfc-5988: "Note that link-values can convey multiple links between the same target and context IRIs; for
     * example:

     *

     * Link: <http://example.org/>
     *       rel="start http://example.net/relation/other"
     *

     * Here, the link to 'http://example.org/' has the registered relation type 'start' and the extension relation type
     * 'http://example.net/relation/other'."
     *
     * @return affordance
     * @see Web Linking Examples
     */
    public Affordance build() {
        Assert.state(!(rels.isEmpty() && reverseRels.isEmpty()),
                "no rels or reverse rels found, call rel() or rev() before building the affordance");
        final Affordance affordance;
        affordance = new Affordance(new PartialUriTemplate(this.toString()), actionDescriptors,
                rels.toArray(new String[rels.size()]));
        for (Map.Entry<String, List<String>> linkParamEntry : linkParams.entrySet()) {
            final List<String> values = linkParamEntry.getValue();
            for (String value : values) {
                affordance.addLinkParam(linkParamEntry.getKey(), value);
            }
        }
        for (String reverseRel : reverseRels) {
            affordance.addRev(reverseRel);
        }
        affordance.setCollectionHolder(collectionHolder);
        return affordance;
    }

    /**
     * Allows to define one or more reverse link relations (a "rev" in terms of rfc-5988), where the resource that has
     * the affordance will be considered the object in a subject-predicate-object statement.
E.g. if you had a rel
     * ex:parent which connects a child to its father, you could also use ex:parent on the father to point
     * to the child by reverting the direction of ex:parent. This is mainly useful when you have no other way to
     * express
     * in your context that the direction of a relationship is inverted.

     *
     * @param rev
     *         to be used as reverse relationship
     * @param revertedRel
     *         to be used in contexts which have no notion of reverse relationships. E.g. for a reverse rel
     *         ex:parent you can use a made-up rel name ex:child which will be used as rel
     *         when rendering HAL.
     * @return builder
     */
    public AffordanceBuilder reverseRel(String rev, String revertedRel) {
        this.rels.add(revertedRel);
        this.reverseRels.add(rev);
        return this;
    }

    /**
     * Allows to define one or more reverse link relations (a "rev" in terms of rfc-5988) to collections in cases where
     * the resource that has the affordance is not the object in a subject-predicate-object statement about each
     * collection item. See {@link #rel(TypedResource, String)} for explanation.
     *
     * @param rev
     *         to be used as reverse relationship
     * @param revertedRel
     *         to be used in contexts which have no notion of reverse relationships, e.g. HAL
     * @param object
     *         describing the object
     * @return builder
     */
    public AffordanceBuilder reverseRel(String rev, String revertedRel, TypedResource object) {
        this.collectionHolder = object;
        this.rels.add(0, revertedRel);
        this.reverseRels.add(rev);
        return this;
    }

    /**
     * Allows to define one or more link relations for the affordance.
     *
     * @param rel
     *         to be used as link relation
     * @return builder
     */
    public AffordanceBuilder rel(String rel) {
        this.rels.add(rel);
        return this;
    }

    /**
     * Allows to define one or more link relations for affordances that point to collections in cases where the
     * resource
     * that has the affordance is not the subject in a subject-predicate-object statement about each collection item.
     * E.g. a product might have a loose relationship to ordered items where it can be POSTed, but the ordered items do
     * not belong to the product, but to an order. You can express that by saying:
     *

     * TypedResource order = new TypedResource("http://schema.org/Order"); // holds the ordered items
     * Resource<Product> product = new Resource<>(); // has a loose relationship to ordered items
     * product.add(linkTo(methodOn(OrderController.class).postOrderedItem()
     *    .rel(order, "orderedItem")); // order has ordered items, not product has ordered items
     *

     * If the order doesn't exist yet, it cannot be identified. In that case you may pass null to express that
     *
     * @param rel
     *         to be used as link relation
     * @param subject
     *         describing the subject
     * @return builder
     */
    public AffordanceBuilder rel(TypedResource subject, String rel) {
        this.collectionHolder = subject;
        this.rels.add(rel);
        return this;
    }

    public AffordanceBuilder withTitle(String title) {
        this.linkParams.set("title", title);
        return this;
    }

    public AffordanceBuilder withTitleStar(String titleStar) {
        this.linkParams.set("title*", titleStar);
        return this;
    }

    /**
     * Allows to define link header params (not UriTemplate variables).
     *
     * @param name
     *         of the link header param
     * @param value
     *         of the link header param
     * @return builder
     */
    public AffordanceBuilder withLinkParam(String name, String value) {
        this.linkParams.add(name, value);
        return this;
    }

    public AffordanceBuilder withAnchor(String anchor) {
        this.linkParams.set("anchor", anchor);
        return this;
    }

    public AffordanceBuilder withHreflang(String hreflang) {
        this.linkParams.add("hreflang", hreflang);
        return this;
    }

    public AffordanceBuilder withMedia(String media) {
        this.linkParams.set("media", media);
        return this;
    }

    public AffordanceBuilder withType(String type) {
        this.linkParams.set("type", type);
        return this;
    }

    @Override
    public AffordanceBuilder slash(Object object) {

        if (object == null) {
            return this;
        }

        if (object instanceof Identifiable) {
            return slash((Identifiable<?>) object);
        }

        String urlPart = object.toString();

        // make sure one cannot delete the fragment
        if (urlPart.endsWith("#")) {
            urlPart = urlPart.substring(0, urlPart.length() - 1);
        }

        if (!StringUtils.hasText(urlPart)) {
            return this;
        }

        final PartialUriTemplateComponents urlPartComponents = new PartialUriTemplate(urlPart).expand(Collections
                .<String, Object>emptyMap());
        final PartialUriTemplateComponents affordanceComponents = partialUriTemplateComponents;

        final String path = !affordanceComponents.getBaseUri()
                .endsWith("/") && !urlPartComponents.getBaseUri()
                .startsWith("/") ?
                affordanceComponents.getBaseUri() + "/" + urlPartComponents.getBaseUri() :
                affordanceComponents.getBaseUri() + urlPartComponents.getBaseUri();
        final String queryHead = affordanceComponents.getQueryHead() +
                (StringUtils.hasText(urlPartComponents.getQueryHead()) ?
                        "&" + urlPartComponents.getQueryHead()
                                .substring(1) :
                        "");
        final String queryTail = affordanceComponents.getQueryTail() +
                (StringUtils.hasText(urlPartComponents.getQueryTail()) ?
                        "," + urlPartComponents.getQueryTail() :
                        "");
        final String fragmentIdentifier = StringUtils.hasText(urlPartComponents.getFragmentIdentifier()) ?
                urlPartComponents.getFragmentIdentifier() :
                affordanceComponents.getFragmentIdentifier();

        List<String> variableNames = new ArrayList<String>();
        variableNames.addAll(affordanceComponents.getVariableNames());
        variableNames.addAll(urlPartComponents.getVariableNames());
        final PartialUriTemplateComponents mergedUriComponents =
                new PartialUriTemplateComponents(path, queryHead, queryTail, fragmentIdentifier, variableNames);

        return new AffordanceBuilder(mergedUriComponents, actionDescriptors);
    }

    @Override
    public AffordanceBuilder slash(Identifiable<?> identifiable) {
        if (identifiable == null) {
            return this;
        }

        return slash(identifiable.getId());
    }

    @Override
    public URI toUri() {
        PartialUriTemplate partialUriTemplate = new PartialUriTemplate(partialUriTemplateComponents.toString());

        final String actionLink = partialUriTemplate.stripOptionalVariables(actionDescriptors)
                .toString();

        if (actionLink == null || actionLink.contains("{")) {
            throw new IllegalStateException("cannot convert template to URI");
        }
        return UriComponentsBuilder.fromUriString(actionLink)
                .build()
                .toUri();
    }

    @Override
    public Affordance withRel(String rel) {
        return rel(rel).build();
    }

    @Override
    public Affordance withSelfRel() {
        return rel(Link.REL_SELF).build();
    }

    @Override
    public String toString() {
        return partialUriTemplateComponents.toString();
    }

    /**
     * Returns a {@link UriComponentsBuilder} obtained from the current servlet mapping with scheme tweaked in case the
     * request contains an {@code X-Forwarded-Ssl} header, which is not (yet) supported by the underlying
     * {@link UriComponentsBuilder}. If no {@link RequestContextHolder} exists (you're outside a Spring Web call), fall
     * back to relative URIs.
     *
     * @return
     */
    static UriComponentsBuilder getBuilder() {
        if (RequestContextHolder.getRequestAttributes() == null) {
            return UriComponentsBuilder.fromPath("/");
        }

        HttpServletRequest request = getCurrentRequest();
        UriComponentsBuilder builder = ServletUriComponentsBuilder.fromServletMapping(request);

        // special case handling for X-Forwarded-Ssl:
        // apply it, but only if X-Forwarded-Proto is unset.

        String forwardedSsl = request.getHeader("X-Forwarded-Ssl");
        ForwardedHeader forwarded = ForwardedHeader.of(request.getHeader(ForwardedHeader.NAME));
        String proto = hasText(forwarded.getProto()) ? forwarded.getProto() : request.getHeader("X-Forwarded-Proto");

        if (!hasText(proto) && hasText(forwardedSsl) && forwardedSsl.equalsIgnoreCase("on")) {
            builder.scheme("https");
        }

        return builder;
    }

    /**
     * Copy of {@link ServletUriComponentsBuilder#getCurrentRequest()} until SPR-10110 gets fixed.
     *
     * @return request
     */
    private static HttpServletRequest getCurrentRequest() {

        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        Assert.state(requestAttributes != null, "Could not find current request via RequestContextHolder");
        Assert.isInstanceOf(ServletRequestAttributes.class, requestAttributes);
        HttpServletRequest servletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
        Assert.state(servletRequest != null, "Could not find current HttpServletRequest");
        return servletRequest;
    }

    /**
     * Adds actionDescriptors of the given AffordanceBuilder to this affordanceBuilder.
     *
     * @param affordanceBuilder
     *         whose action descriptors should be added to this one
     * @return builder
     */
    public AffordanceBuilder and(AffordanceBuilder affordanceBuilder) {
        for (ActionDescriptor actionDescriptor : affordanceBuilder.actionDescriptors) {
            this.actionDescriptors.add(actionDescriptor);
        }
        return this;
    }
}


/*
 * Copyright (c) 2014. Escalon System-Entwicklung, Dietrich Schulten
 *
 * 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 de.escalon.hypermedia.affordance;

import de.escalon.hypermedia.spring.AffordanceBuilder;
import org.springframework.hateoas.TemplateVariable;
import org.springframework.util.Assert;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * URI template with the ability to be partially expanded, no matter if its variables are required or not. Unsatisfied
 * variables are kept as variables. Other implementations either remove all unsatisfied variables or fail when required
 * variables are unsatisfied. This behavior is required due to the way an Affordance is created by {@link
 * AffordanceBuilder}, see package info for an overview of affordance creation.
 *
 * @author Dietrich Schulten
 * @see de.escalon.hypermedia.spring
 */
public class PartialUriTemplate {

    private static final Pattern VARIABLE_REGEX = Pattern.compile("\\{([\\?\\&#/]?)([\\w\\,\\.]+)(:??.*?)\\}");

    private final List<String> urlComponents = new ArrayList<String>();

    private final List<List<Integer>> variableIndices = new ArrayList<List<Integer>>();
    private List<TemplateVariable> variables = new ArrayList<TemplateVariable>();
    private List<String> variableNames = new ArrayList<String>();

    /**
     * Creates a new {@link PartialUriTemplate} using the given template string.
     *
     * @param template
     *         must not be {@literal null} or empty.
     */
    public PartialUriTemplate(String template) {
        Assert.hasText(template, "Template must not be null or empty!");

        Matcher matcher = VARIABLE_REGEX.matcher(template);
        // first group is the variable start without leading {: "", "/", "?", "#",
        // second group is the comma-separated name list without the trailing } of the variable
        int endOfPart = 0;
        while (matcher.find()) {

            // 0 is the current match, i.e. the entire variable expression
            int startOfPart = matcher.start(0);
            // add part before current match
            if (endOfPart < startOfPart) {
                final String partWithoutVariables = template.substring(endOfPart, startOfPart);
                final StringTokenizer stringTokenizer = new StringTokenizer(partWithoutVariables, "?", true);
                boolean inQuery = false;
                while (stringTokenizer.hasMoreTokens()) {
                    final String token = stringTokenizer.nextToken();
                    if ("?".equals(token)) {
                        inQuery = true;
                    } else {
                        if (!inQuery) {
                            urlComponents.add(token);
                        } else {
                            urlComponents.add("?" + token);
                        }
                        variableIndices.add(Collections.<Integer>emptyList());
                    }
                }
            }
            endOfPart = matcher.end(0);

            // add current match as part
            final String variablePart = template.substring(startOfPart, endOfPart);
            urlComponents.add(variablePart);

            // collect variablesInPart and track for each part which variables it contains
            // group(1) is the variable head without the leading {
            TemplateVariable.VariableType type = TemplateVariable.VariableType.from(matcher.group(1));
            // group(2) are the variable names
            String[] names = matcher.group(2)
                    .split(",");
            List<Integer> variablesInPart = new ArrayList<Integer>();
            for (String name : names) {
                TemplateVariable variable = new TemplateVariable(name, type);
                variablesInPart.add(variables.size());
                variables.add(variable);
                variableNames.add(name);
            }
            variableIndices.add(variablesInPart);
        }
        // finish off remaining part
        if (endOfPart < template.length()) {
            urlComponents.add(template.substring(endOfPart));
            variableIndices.add(Collections.<Integer>emptyList());
        }
    }

    public List<String> getVariableNames() {
        return variableNames;
    }

    /**
     * Returns the template as uri components, without variable expansion.
     *
     * @return components of the Uri
     */
    public PartialUriTemplateComponents asComponents() {
        return getUriTemplateComponents(Collections.<String, Object>emptyMap(), Collections.<String>emptyList());
    }

    /**
     * Expands the template using given parameters
     *
     * @param parameters
     *         for expansion in the order of appearance in the template, must not be empty
     * @return expanded template
     */
    public PartialUriTemplateComponents expand(Object... parameters) {
        List<String> variableNames = getVariableNames();
        Map<String, Object> parameterMap = new LinkedHashMap<String, Object>();

        int i = 0;
        for (String variableName : variableNames) {
            if (i < parameters.length) {
                parameterMap.put(variableName, parameters[i++]);
            } else {
                break;
            }
        }
        return getUriTemplateComponents(parameterMap, Collections.<String>emptyList());
    }

    /**
     * Expands the template using given parameters
     *
     * @param parameters
     *         for expansion, must not be empty
     * @return expanded template
     */
    public PartialUriTemplateComponents expand(Map<String, ?> parameters) {
        return getUriTemplateComponents(parameters, Collections.<String>emptyList());
    }

    /**
     * Applies parameters to template variables.
     *
     * @param parameters
     *         to apply to variables
     * @param requiredArgs
     *         if not empty, retains given requiredArgs
     * @return uri components
     */
    private PartialUriTemplateComponents getUriTemplateComponents(Map<String, ?> parameters, List<String>
            requiredArgs) {
        Assert.notNull(parameters, "Parameters must not be null!");

        final StringBuilder baseUrl = new StringBuilder(urlComponents.get(0));
        final StringBuilder queryHead = new StringBuilder();
        final StringBuilder queryTail = new StringBuilder();
        final StringBuilder fragmentIdentifier = new StringBuilder();
        for (int i = 1; i < urlComponents.size(); i++) {
            final String part = urlComponents.get(i);
            final List<Integer> variablesInPart = variableIndices.get(i);
            if (variablesInPart.isEmpty()) {
                if (part.startsWith("?") || part.startsWith("&")) {
                    queryHead.append(part);
                } else if (part.startsWith("#")) {
                    fragmentIdentifier.append(part);
                } else {
                    baseUrl.append(part);
                }
            } else {
                for (Integer variableInPart : variablesInPart) {
                    final TemplateVariable variable = variables.get(variableInPart);
                    final Object value = parameters.get(variable.getName());
                    if (value == null) {
                        switch (variable.getType()) {
                            case REQUEST_PARAM:
                            case REQUEST_PARAM_CONTINUED:
                                if (requiredArgs.isEmpty() || requiredArgs.contains(variable.getName())) {
                                    // query vars without value always go last (query tail)
                                    if (queryTail.length() > 0) {
                                        queryTail.append(',');
                                    }
                                    queryTail.append(variable.getName());
                                }
                                break;
                            case FRAGMENT:
                                fragmentIdentifier.append(variable.toString());
                                break;
                            case PATH_VARIABLE:
                                if (queryHead.length() != 0) {
                                    // level 1 variable in query
                                    queryHead.append(variable.toString());
                                } else {
                                    baseUrl.append(variable.toString());
                                }
                                break;
                            case SEGMENT:
                                baseUrl.append(variable.toString());
                        }
                    } else {
                        switch (variable.getType()) {
                            case REQUEST_PARAM:
                            case REQUEST_PARAM_CONTINUED:
                                if (queryHead.length() == 0) {
                                    queryHead.append('?');
                                } else {
                                    queryHead.append('&');
                                }
                                queryHead.append(variable.getName())
                                        .append('=')
                                        .append(urlEncode(value.toString()));
                                break;
                            case SEGMENT:
                                baseUrl.append('/');
                                // fall through
                            case PATH_VARIABLE:
                                if (queryHead.length() != 0) {
                                    // level 1 variable in query
                                    queryHead.append(urlEncode(value.toString()));
                                } else {
                                    baseUrl.append(urlEncode(value.toString()));
                                }
                                break;
                            case FRAGMENT:
                                fragmentIdentifier.append('#');
                                fragmentIdentifier.append(urlEncode(value.toString()));
                                break;
                        }
                    }
                }
            }
        }

        return new PartialUriTemplateComponents(baseUrl.toString(), queryHead.toString(), queryTail.toString(),
                fragmentIdentifier.toString(), variableNames);
    }

    private String urlEncode(String s) {
        try {
            return URLEncoder.encode(s, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException("failed to urlEncode " + s, e);
        }
    }

    /**
     * Strips all variables which are not required by any of the given action descriptors. If no action descriptors are
     * given, nothing will be stripped.
     *
     * @param actionDescriptors
     *         to decide which variables are optional, may be empty
     * @return partial uri template components without optional variables, if actionDescriptors was not empty
     */
    public PartialUriTemplateComponents stripOptionalVariables(List<ActionDescriptor> actionDescriptors) {
        return getUriTemplateComponents(Collections.<String, Object>emptyMap(), getRequiredArgNames(actionDescriptors));
    }

    private List<String> getRequiredArgNames(List<ActionDescriptor> actionDescriptors) {
        List<String> ret = new ArrayList<String>();
        for (ActionDescriptor actionDescriptor : actionDescriptors) {
            Map<String, ActionInputParameter> required = actionDescriptor.getRequiredParameters();
            for (ActionInputParameter actionInputParameter : required.values()) {
                ret.add(actionInputParameter.getParameterName());
            }
        }
        return ret;
    }
}


/*
 * Copyright (c) 2015. Escalon System-Entwicklung, Dietrich Schulten
 *
 * 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 de.escalon.hypermedia.spring.uber;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import de.escalon.hypermedia.affordance.ActionDescriptor;
import de.escalon.hypermedia.affordance.PartialUriTemplate;
import de.escalon.hypermedia.affordance.PartialUriTemplateComponents;
import org.springframework.hateoas.Link;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

public abstract class AbstractUberNode implements Iterable<UberNode> {

    protected List<UberNode> data = new ArrayList<UberNode>();

    public AbstractUberNode() {
        super();
    }

    @JsonInclude(Include.NON_EMPTY)
    public List<UberNode> getData() {
        return data;
    }

    public void addData(UberNode item) {
        data.add(item);
    }

    public void addLink(Link link) {
        List<ActionDescriptor> actionDescriptors = UberUtils.getActionDescriptors(link);
        List<String> rels = UberUtils.getRels(link);
        PartialUriTemplateComponents partialUriTemplateComponents = new PartialUriTemplate(link.getHref()).expand
                (Collections.<String, Object>emptyMap());
        for (ActionDescriptor actionDescriptor : actionDescriptors) {
            UberNode uberLink = UberUtils.toUberLink(link.getHref(), actionDescriptor, rels);
            data.add(uberLink);
        }
    }

    public void addLinks(Iterable<Link> links) {
        for (Link link : links) {
            addLink(link);
        }
    }

    /**
     * Gets first child of this uber node having the given name attribute.
     *
     * @param name
     *         to look up
     * @return found child or null
     */
    public UberNode getFirstByName(String name) {
        // TODO consider less naive impl
        UberNode ret = null;
        for (UberNode node : data) {
            if (name.equals(node.getName())) {
                ret = node;
                break;
            }
        }
        return ret;
    }

    /**
     * Gets first child of this uber node having the given rel attribute.
     *
     * @param rel
     *         to look up
     * @return found child or null
     */
    public UberNode getFirstByRel(String rel) {
        // TODO consider less naive impl
        for (UberNode node : data) {
            List<String> myRels = node.getRel();
            if (myRels != null) {
                for (String myRel : myRels) {
                    if (rel.equals(myRel)) {
                        return node;
                    }
                }
            }
        }
        return null;
    }

    /**
     * Allows iterating over children of this uber node which have a data attribute.
     */
    @Override
    public Iterator<UberNode> iterator() {

        return new Iterator<UberNode>() {

            int index = 0;

            @Override
            public void remove() {
                throw new UnsupportedOperationException("removing from uber node is not supported");
            }

            @Override
            public UberNode next() {
                index = findNextChildWithData();
                return data.get(index++);
            }

            @Override
            public boolean hasNext() {
                return findNextChildWithData() != -1;
            }

            private int findNextChildWithData() {
                for (int i = index; i < data.size(); i++) {
                    if (!data.get(i)
                            .getData()
                            .isEmpty()) {
                        return i;
                    }
                }
                return -1;
            }
        };
    }
}


/*
 *   Copyright (c) Rich Hickey. All rights reserved.
 *   The use and distribution terms for this software are covered by the
 *   Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
 *   which can be found in the file epl-v10.html at the root of this distribution.
 *   By using this software in any fashion, you are agreeing to be bound by
 * 	 the terms of this license.
 *   You must not remove this notice, or any other, from this software.
 **/

/* rich Mar 3, 2008 */
/* glen added types 2015-06-06 any errors are now mine. */

package org.organicdesign.fp.collections;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Map;

/**
 A wrapper that turns a PersistentTreeMap into a set.

 This file is a derivative work based on a Clojure collection licensed under the Eclipse Public
 License 1.0 Copyright Rich Hickey
*/
public class PersistentHashSet<E> extends AbstractUnmodSet<E>
        implements ImSet<E>, Serializable {

    // If you don't put this here, it inherits EMPTY from UnmodSet, which does not have .equals()
    // defined.  UnmodSet.empty won't put() either.
    public static final PersistentHashSet<Object> EMPTY =
            new PersistentHashSet<>(PersistentHashMap.EMPTY);

    @SuppressWarnings("unchecked")
    public static <E> PersistentHashSet<E> empty() { return (PersistentHashSet<E>) EMPTY; }

    /** Works around some type inference limitations of Java 8. */
    public static <E> MutableHashSet<E> emptyMutable() {
        return PersistentHashSet.<E>empty().mutable();
    }

    public static <E> PersistentHashSet<E> empty(Equator<E> eq) {
        return new PersistentHashSet<>(PersistentHashMap.empty(eq));
    }

    /** Works around some type inference limitations of Java 8. */
    public static <E> MutableHashSet<E> emptyMutable(Equator<E> eq) {
        return empty(eq).mutable();
    }

    /**
     Returns a new PersistentHashSet of the values.  The vararg version of this method is
     {@link org.organicdesign.fp.StaticImports#set(Object...)}   If the input contains duplicate
     elements, later values overwrite earlier ones.

     @param elements The items to put into a vector.
     @return a new PersistentHashSet of the given elements.
     */
    public static <E> PersistentHashSet<E> of(Iterable<E> elements) {
        PersistentHashSet<E> empty = empty();
        MutableSet<E> ret = empty.mutable();
        for (E e : elements) {
            ret.put(e);
        }
        return (PersistentHashSet<E>) ret.immutable();
    }

    public static <E> PersistentHashSet<E> ofEq(Equator<E> eq, Iterable<E> init) {
        MutableSet<E> ret = emptyMutable(eq);
        for (E e : init) {
            ret.put(e);
        }
        return (PersistentHashSet<E>) ret.immutable();
    }

    @SuppressWarnings("unchecked")
    public static <E> PersistentHashSet<E> ofMap(ImMap<E,?> map) {
        return new PersistentHashSet<>((ImMap<E,E>) map);
    }

    // ==================================== Instance Variables ====================================
    private final ImMap<E,E> impl;

    // ======================================= Constructor =======================================
    private PersistentHashSet(ImMap<E,E> i) { impl = i; }

    // ======================================= Serialization =======================================
    // This class has a custom serialized form designed to be as small as possible.  It does not
    // have the same internal structure as an instance of this class.

    // For serializable.  Make sure to change whenever internal data format changes.
    private static final long serialVersionUID = 20160904155600L;

    // Check out Josh Bloch Item 78, p. 312 for an explanation of what's going on here.
    private static class SerializationProxy<K> implements Serializable {
        // For serializable.  Make sure to change whenever internal data format changes.
        private static final long serialVersionUID = 20160904155600L;

        private final int size;
        private transient ImMap<K,K> theMap;
        SerializationProxy(ImMap<K,K> phm) {
            size = phm.size();
            theMap = phm;
        }

        // Taken from Josh Bloch Item 75, p. 298
        private void writeObject(ObjectOutputStream s) throws IOException {
            s.defaultWriteObject();
            // Write out all elements in the proper order
            for (Map.Entry<K,?> entry : theMap) {
                s.writeObject(entry.getKey());
            }
        }

        @SuppressWarnings("unchecked")
        private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
            s.defaultReadObject();
            MutableMap tempMap = PersistentHashMap.<K,K>empty().mutable();
            for (int i = 0; i < size; i++) {
                K k = (K) s.readObject();
                tempMap = tempMap.assoc(k, k);
            }
            theMap = tempMap.immutable();
        }

        private Object readResolve() { return PersistentHashSet.ofMap(theMap); }
    }

    private Object writeReplace() { return new SerializationProxy<>(impl); }

    private void readObject(java.io.ObjectInputStream in) throws IOException,
            ClassNotFoundException {
        throw new InvalidObjectException("Proxy required");
    }

    // ===================================== Instance Methods =====================================
    @Override public boolean contains(Object key) {
        //noinspection SuspiciousMethodCalls
        return impl.containsKey(key);
    }

    /** Returns the Equator used by this set for equals comparisons and hashCodes */
    public Equator<E> equator() { return impl.equator(); }

    @Override public PersistentHashSet<E> without(E key) {
        if (contains(key))
            return new PersistentHashSet<>(impl.without(key));
        return this;
    }

    @Override public PersistentHashSet<E> put(E o) {
        if (contains(o))
            return this;
        return new PersistentHashSet<>(impl.assoc(o, o));
    }

//    @Override public Sequence<E> seq() { return impl.seq().map(e -> e.getKey()); }

    @Override public UnmodIterator<E> iterator() { return impl.keyIterator(); }

    @Override public int size() { return impl.size(); }

    public MutableHashSet<E> mutable() {
        return new MutableHashSet<>(impl.mutable());
    }

    public static final class MutableHashSet<E> extends AbstractUnmodSet<E>
            implements MutableSet<E> {

        MutableMap<E,E> impl;

        MutableHashSet(MutableMap<E,E> impl) { this.impl = impl; }

        @Override public int size() { return impl.size(); }

        @Override public MutableHashSet<E> put(E val) {
            MutableMap<E,E> m = impl.assoc(val, val);
            if (m != impl) this.impl = m;
            return this;
        }

        @Override
        public UnmodIterator<E> iterator() { return impl.keyIterator(); }

        @SuppressWarnings("unchecked")
        @Override public boolean contains(Object key) {
            return impl.entry((E) key).isSome();
        }

        @Override public MutableHashSet<E> without(E key) {
            MutableMap<E,E> m = impl.without(key);
            if (m != impl) this.impl = m;
            return this;
        }

        @Override  public PersistentHashSet<E> immutable() {
            return new PersistentHashSet<>(impl.immutable());
        }
    }

}