public interface ContentNegotiator {

	boolean isSupportedContentTypes(List<String> contentTypes);

	List<String> contentTypes();

} public class DefaultContentNegotiator implements ContentNegotiator {

	private static final Logger logger = LoggerFactory.getLogger(DefaultContentNegotiator.class);

	public static final String DEFAULT_FAVORED_PARAMETER_NAME = "format";
	public static final List<String> DEFAULT_SUPPORTED_CONTENT_TYPES = unmodifiableList(asList("text/html"));
	public static final String ACCEPT_HEADER = "Accept";

    private static final String CONTENT_TYPE_DELIMITER = ",\\s*";

	private List<String> supportedContentTypes;
	private boolean favorParameterOverAcceptHeader;
	private String favoredParameterName;
	private Comparator<List<String>> contentTypeComparator;

	public DefaultContentNegotiator() {
		supportedContentTypes = DEFAULT_SUPPORTED_CONTENT_TYPES;
		favorParameterOverAcceptHeader = false;
		favoredParameterName = DEFAULT_FAVORED_PARAMETER_NAME;
		contentTypeComparator = new DefaultContentTypeComparator();
	}

	public List<String> getSupportedContentTypes() {
		return supportedContentTypes;
	}

	public void setSupportedContentTypes(final List<String> supportedContentTypes) {
		this.supportedContentTypes = supportedContentTypes;
	}

	public boolean isFavorParameterOverAcceptHeader() {
		return favorParameterOverAcceptHeader;
	}

	public void setFavorParameterOverAcceptHeader(final boolean favorParameterOverAcceptHeader) {
		this.favorParameterOverAcceptHeader = favorParameterOverAcceptHeader;
	}

	public String getFavoredParameterName() {
		return favoredParameterName;
	}

	public void setFavoredParameterName(final String parameterName) {
		this.favoredParameterName = parameterName;
	}

	public Comparator<List<String>> getContentTypeComparator() {
		return contentTypeComparator;
	}

	public void setContentTypeComparator(final Comparator<List<String>> contentTypeComparator) {
		this.contentTypeComparator = contentTypeComparator;
	}

	/**
	 * Returns true if able to support the requested content types; false otherwise.
	 * 
	 * @return True if able to support the requested content types; false otherwise.
	 */
	@Override
	public boolean isSupportedContentTypes(final List<String> contentTypes) {
		return contentTypeComparator.compare(supportedContentTypes, contentTypes) == 0;
	}

	/**
	 * Returns requested content types or default content type if none found.
	 * 
	 * @return Requested content types or default content type if none found.
	 */
	@Override
	public List<String> contentTypes() {
		List<String> contentTypes = null;
		final HttpServletRequest request = getHttpRequest();

		if (favorParameterOverAcceptHeader) {
			contentTypes = getFavoredParameterValueAsList(request);
		} else {
			contentTypes = getAcceptHeaderValues(request);
		}

		if (isEmpty(contentTypes)) {
			logger.debug("Setting content types to default: {}.", DEFAULT_SUPPORTED_CONTENT_TYPES);

			contentTypes = DEFAULT_SUPPORTED_CONTENT_TYPES;
		}

		return unmodifiableList(contentTypes);
	}

	private HttpServletRequest getHttpRequest() {
		final RequestAttributes attrs = getRequestAttributes();
		isInstanceOf(ServletRequestAttributes.class, attrs);

		return ((ServletRequestAttributes) attrs).getRequest();
	}

	private List<String> getFavoredParameterValueAsList(HttpServletRequest request) {
		logger.debug("Retrieving content type from favored parameter: {}.", favoredParameterName);

		final String favoredParameterValue = request.getParameter(favoredParameterName);

		return favoredParameterValue != null ? splitContentTypes(asList(favoredParameterValue)) : null;
	}

	@SuppressWarnings("unchecked")
	private List<String> getAcceptHeaderValues(HttpServletRequest request) {
		logger.debug("Retrieving content type from header: {}.", ACCEPT_HEADER);

		return splitContentTypes(list(request.getHeaders(ACCEPT_HEADER)));
	}

	private List<String> splitContentTypes(final List<String> contentTypes) {
		final List<String> splitContentTypes = new ArrayList<String>();

		for (String aContentType : contentTypes) {
			splitContentTypes.addAll(asList(aContentType.split(CONTENT_TYPE_DELIMITER)));
		}

		logger.debug("Split content types {} into {}.", contentTypes, splitContentTypes);

		return splitContentTypes;
	}

	private static final class DefaultContentTypeComparator implements Comparator<List<String>> {
		private static final int NOT_EQUAL = 1;
		private static final int EQUAL = 0;
		private static final String ASTERISK = "*";
		private static final String PERIOD_PLUS_ASTERISK = ".*";

		@Override
		public int compare(List<String> supportedContentTypes, List<String> contentTypes) {
			logger.debug("Comparing supported content types with request content types: {}, {}.",
					supportedContentTypes, contentTypes);

			if (isEmpty(contentTypes)) {
				return NOT_EQUAL;
			}

			for (String aContentType : contentTypes) {
				for (String aSupportedContentType : supportedContentTypes) {
					if (aSupportedContentType.matches(convertToRegex(aContentType))) {
						return EQUAL;
					}
				}
			}

			return NOT_EQUAL;
		}

		private String convertToRegex(final String aContentType) {
			return aContentType.trim().replaceAll(quote(ASTERISK), PERIOD_PLUS_ASTERISK);
		}
	}
} public class SoyTemplateViewResolver extends AbstractCachingViewResolver implements Ordered {

	/**
	 * Prefix for special view names that specify a redirect URL (usually to a controller after a form has been
	 * submitted and processed). Such view names will not be resolved in the configured default way but rather be
	 * treated as special shortcut.
	 */
	public static final String REDIRECT_URL_PREFIX = "redirect:";

	/**
	 * Prefix for special view names that specify a forward URL (usually to a controller after a form has been submitted
	 * and processed). Such view names will not be resolved in the configured default way but rather be treated as
	 * special shortcut.
	 */
	public static final String FORWARD_URL_PREFIX = "forward:";

	private static final Logger logger = getLogger(SoyTemplateViewResolver.class);

	protected TemplateRenderer templateRenderer = new EmptyTemplateRenderer();

	protected ModelAdjuster modelAdjuster = new EmptyModelAdjuster();

	protected GlobalRuntimeModelResolver globalRuntimeModelResolver = new EmptyGlobalRuntimeModelResolver();

	protected LocaleProvider localeProvider = new EmptyLocaleProvider();

	protected SoyMsgBundleResolver soyMsgBundleResolver = new EmptySoyMsgBundleResolver();

	private CompiledTemplatesHolder compiledTemplatesHolder = new EmptyCompiledTemplatesHolder();

	private ContentNegotiator contentNegotiator = new DefaultContentNegotiator();

	/** an encoding to use */
	private String encoding = DEFAULT_ENCODING;

	/** a default view */
	private String indexView = "index";

	private boolean redirectContextRelative = true;

	private boolean redirectHttp10Compatible = true;

	private int order = MAX_VALUE;

	private String prefix = DEFAULT_SOY_PREFIX;

	protected View loadView(final String viewName, final Locale locale) throws Exception {
		checkNotNull(viewName, "viewName cannot be null!");
		checkNotNull(templateRenderer, "templateRenderer cannot be null!");
		logger.debug("loadView:{}, locale:{}", viewName, locale);

		if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
			final String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
			final RedirectView view = new RedirectView(redirectUrl, redirectContextRelative, redirectHttp10Compatible);

			return applyLifecycleMethods(viewName, view);
		}

		if (viewName.startsWith(FORWARD_URL_PREFIX)) {
			final String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());

			return new InternalResourceView(forwardUrl);
		}

		final String newViewName = orIndexView(normalize(viewName));
		
		final List<String> contentTypes = contentNegotiator.contentTypes();

		if (!canHandle(newViewName) || !contentNegotiator.isSupportedContentTypes(contentTypes)) {
			logger.debug("Unable to handle view: {}.", newViewName);

			return null;
		}

		final SoyView view = new SoyView();
		view.setTemplateName(stripPrefix(newViewName));
		view.setContentType(resolveContentType(contentTypes));
		view.setTemplateRenderer(templateRenderer);
		view.setModelAdjuster(modelAdjuster);
		view.setGlobalRuntimeModelResolver(globalRuntimeModelResolver);
		view.setLocaleProvider(localeProvider);
		view.setSoyMsgBundleResolver(soyMsgBundleResolver);
		view.setCompiledTemplates(compiledTemplatesHolder.compiledTemplates());

		return view;
	}

	private boolean canHandle(final String templateName) {
		return templateName.startsWith(prefix);
	}

	private String stripPrefix(final String templateName) {
		if (canHandle(templateName)) {
			return templateName.replace(prefix, "");
		}

		return templateName;
	}

	/**
	 * Map / to a default view name.
	 * 
	 * @param viewName
	 *            The view name.
	 * @return The defaulted view name.
	 */
	private String orIndexView(final String viewName) {
		return isEmpty(viewName) ? indexView : viewName;
	}

	/**
	 * Remove beginning and ending slashes, then replace all occurrences of / with .
	 *
	 * @param viewName
	 *            The Spring viewName
	 * @return The name of the view, dot separated.
	 */
	private String normalize(final String viewName) {
		String normalized = viewName;

		if (normalized.startsWith("/")) {
			normalized = normalized.substring(0);
		}

		if (normalized.endsWith("/")) {
			normalized = normalized.substring(0, normalized.length() - 1);
		}

		return normalized.replaceAll("/", ".");
	}

	private View applyLifecycleMethods(String viewName, AbstractView view) {
		return (View) getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, viewName);
	}
	
	protected String resolveContentType(final List<String> contentTypes) {
		checkArgument(!isEmpty(contentTypes), "Content types must not be null or empty.");
		
        // TODO: This should be better handled.		
		if (contentTypes.size() > 1) {
			logger.warn("More than one content type is not currently supported; "
					+ "only first one ({}) will be used.", contentTypes.get(0));
		}
		
		return contentTypes.get(0) + "; charset=" + encoding;
    }

	public void setTemplateRenderer(final TemplateRenderer templateRenderer) {
		this.templateRenderer = templateRenderer;
	}

	public void setModelAdjuster(final ModelAdjuster modelAdjuster) {
		this.modelAdjuster = modelAdjuster;
	}

	public void setGlobalRuntimeModelResolver(final GlobalRuntimeModelResolver globalRuntimeModelResolver) {
		this.globalRuntimeModelResolver = globalRuntimeModelResolver;
	}

	public void setLocaleProvider(final LocaleProvider localeProvider) {
		this.localeProvider = localeProvider;
	}

	public void setSoyMsgBundleResolver(final SoyMsgBundleResolver soyMsgBundleResolver) {
		this.soyMsgBundleResolver = soyMsgBundleResolver;
	}

	public void setIndexView(String indexView) {
		this.indexView = indexView;
	}

	public void setRedirectContextRelative(boolean redirectContextRelative) {
		this.redirectContextRelative = redirectContextRelative;
	}

	public void setRedirectHttp10Compatible(boolean redirectHttp10Compatible) {
		this.redirectHttp10Compatible = redirectHttp10Compatible;
	}

	public boolean isRedirectContextRelative() {
		return redirectContextRelative;
	}

	public boolean isRedirectHttp10Compatible() {
		return redirectHttp10Compatible;
	}

	public void setCompiledTemplatesHolder(CompiledTemplatesHolder compiledTemplatesHolder) {
		this.compiledTemplatesHolder = compiledTemplatesHolder;
	}

	@Override
	public int getOrder() {
		return this.order;
	}

	public void setOrder(int order) {
		this.order = order;
	}

	public String getPrefix() {
		return prefix;
	}

	public void setPrefix(String prefix) {
		this.prefix = prefix;
	}

	public String getEncoding() {
		return encoding;
	}

	public void setEncoding(String encoding) {
		this.encoding = encoding;
	}

	public String getIndexView() {
		return indexView;
	}

	public void setHotReloadMode(final boolean hotReloadMode) {
		setCache(!hotReloadMode);
	}

    public void setContentNegotiator(ContentNegotiator contentNegotiator) {
        this.contentNegotiator = contentNegotiator;
    }

} public class DefaultSoyMsgBundleResolver implements SoyMsgBundleResolver {

    private static final Logger logger = LoggerFactory.getLogger(DefaultSoyMsgBundleResolver.class);

    /** a cache of soy msg bundles */
    /** friendly */ Map<Locale, SoyMsgBundle> msgBundles = new ConcurrentHashMap<Locale, SoyMsgBundle>();

    /** a path to a bundle */
    private String messagesPath = SoyViewConfigDefaults.DEF_MESSAGES_PATH;

    /** will cache msgBundles if a hotReloadMode is off, if debug is on,
     *  will compile msg bundles each time it is invoked */
    private boolean hotReloadMode = SoyViewConfigDefaults.DEFAULT_HOT_RELOAD_MODE;

    /** in case translation is missing for a passed in locale,
     *  whether the implementation should fallback to English returning
     *  an english translation if available */
    private boolean fallbackToEnglish = true;

    /**
     * Based on a provided locale return a SoyMsgBundle file.
     *
     * If a passed in locale object is "Optional.absent()",
     * the implementation will return Optional.absent() as well
     * @param locale - maybe locale
     * @return maybe soy msg bundle
     */
    public Optional<SoyMsgBundle> resolve(final Optional<Locale> locale) throws IOException {
        if (!locale.isPresent()) {
            return Optional.absent();
        }

        synchronized (msgBundles) {
            SoyMsgBundle soyMsgBundle = null;
            if (isHotReloadModeOff()) {
                soyMsgBundle = msgBundles.get(locale.get());
            }
            if (soyMsgBundle == null) {
                soyMsgBundle = createSoyMsgBundle(locale.get());
                if (soyMsgBundle == null) {
                    soyMsgBundle = createSoyMsgBundle(new Locale(locale.get().getLanguage()));
                }

                if (soyMsgBundle == null && fallbackToEnglish) {
                    soyMsgBundle = createSoyMsgBundle(Locale.ENGLISH);
                }

                if (soyMsgBundle == null) {
                    return Optional.absent();
                }

                if (isHotReloadModeOff()) {
                    msgBundles.put(locale.get(), soyMsgBundle);
                }
            }

            return Optional.fromNullable(soyMsgBundle);
        }
    }

    /**
     * An implementation that using a ContextClassLoader iterates over all urls it finds
     * based on a messagePath and locale, e.g. messages_de_DE.xlf and returns a merged
     * SoyMsgBundle of SoyMsgBundle matching a a pattern it finds in a class path.
     *
     * @param locale - locale
     * @return SoyMsgBundle - bundle
     * @throws java.io.IOException - io error
     */
    protected SoyMsgBundle createSoyMsgBundle(final Locale locale) throws IOException {
        Preconditions.checkNotNull(messagesPath, "messagesPath cannot be null!");
        final String path = messagesPath  + "_" + locale.toString() + ".xlf";

        final Enumeration<URL> e = Thread.currentThread().getContextClassLoader().getResources(path);

        final List<SoyMsgBundle> msgBundles = Lists.newArrayList();

        final SoyMsgBundleHandler msgBundleHandler = new SoyMsgBundleHandler(new XliffMsgPlugin());

        while (e.hasMoreElements()) {
            final URL msgFile = e.nextElement();
            final SoyMsgBundle soyMsgBundle = msgBundleHandler.createFromResource(msgFile);
            msgBundles.add(soyMsgBundle);
        }

        return mergeMsgBundles(locale, msgBundles).orNull();
    }

    /**
     * Merge msg bundles together, creating new MsgBundle with merges msg bundles passed in as a method argument
     */
    private Optional<? extends SoyMsgBundle> mergeMsgBundles(final Locale locale, final List<SoyMsgBundle> soyMsgBundles) {
        if (soyMsgBundles.isEmpty()) {
            return Optional.absent();
        }

        final List<SoyMsg> msgs = Lists.newArrayList();
        for (final SoyMsgBundle smb : soyMsgBundles) {
            for (final Iterator<SoyMsg> it = smb.iterator(); it.hasNext();) {
                msgs.add(it.next());
            }
        }

        return Optional.of(new SoyMsgBundleImpl(locale.toString(), msgs));
    }

    public void setMessagesPath(final String messagesPath) {
        this.messagesPath = messagesPath;
    }

    public void setHotReloadMode(final boolean hotReloadMode) {
        this.hotReloadMode = hotReloadMode;
    }

    public void setFallbackToEnglish(boolean fallbackToEnglish) {
        this.fallbackToEnglish = fallbackToEnglish;
    }

    public String getMessagesPath() {
        return messagesPath;
    }

    public boolean isHotReloadMode() {
        return hotReloadMode;
    }

    private boolean isHotReloadModeOff() {
        return !hotReloadMode;
    }

    public boolean isFallbackToEnglish() {
        return fallbackToEnglish;
    }

} public interface SoyMsgBundleResolver {

    Optional<SoyMsgBundle> resolve(Optional<Locale> locale) throws IOException;

} public class DefaultTofuCompiler implements TofuCompiler {

    private static final Logger logger = LoggerFactory.getLogger(DefaultTofuCompiler.class);

    private boolean hotReloadMode = SoyViewConfigDefaults.DEFAULT_HOT_RELOAD_MODE;


    private CompileTimeGlobalModelResolver compileTimeGlobalModelResolver = new EmptyCompileTimeGlobalModelResolver();

    private SoyJsSrcOptions soyJsSrcOptions = new SoyJsSrcOptions();

    private SoyTofuOptions soyTofuOptions = createSoyTofuOptions();

    @Override
    public SoyTofu compile(@Nullable final Collection<URL> urls) throws IOException {
        Preconditions.checkNotNull("compileTimeGlobalModelResolver", compileTimeGlobalModelResolver);

        if (urls == null || urls.isEmpty()) {
            throw new IOException("Unable to compile, no urls found");
        }

        logger.debug("SoyTofu compilation of templates:" + urls.size());
        final long time1 = System.currentTimeMillis();

        final SoyFileSet.Builder sfsBuilder = new SoyFileSet.Builder();

        for (final URL url : urls) {
            sfsBuilder.add(url);
        }

        addCompileTimeGlobalModel(sfsBuilder);

        final SoyFileSet soyFileSet = sfsBuilder.build();

        final SoyTofuOptions soyTofuOptions = createSoyTofuOptions();
        final SoyTofu soyTofu = soyFileSet.compileToTofu(soyTofuOptions);

        final long time2 = System.currentTimeMillis();

        logger.debug("SoyTofu compilation complete." + (time2 - time1) + " ms");

        return soyTofu;
    }

    private void addCompileTimeGlobalModel(final SoyFileSet.Builder sfsBuilder) {
        final Optional<SoyMapData> soyMapData = compileTimeGlobalModelResolver.resolveData();
        if (soyMapData.isPresent()) {
            final Map<String, ?> mapData = soyMapData.get().asMap();
            if (mapData.size() > 0) {
                logger.debug("Setting compile time globals, entries number:" + mapData.size());
                sfsBuilder.setCompileTimeGlobals(mapData);
            }
        }
    }

    private SoyTofuOptions createSoyTofuOptions() {
        final SoyTofuOptions soyTofuOptions = new SoyTofuOptions();
        soyTofuOptions.setUseCaching(isHotReloadModeOff());

        return soyTofuOptions;
    }

    @Override
    public final Optional<String> compileToJsSrc(@Nullable final URL url, @Nullable final SoyMsgBundle soyMsgBundle) {
        if (url == null) {
            return Optional.absent();
        }

        final Collection<String> compiledTemplates = compileToJsSrc(Lists.newArrayList(url), soyMsgBundle);
        if (compiledTemplates.isEmpty()) {
            return Optional.absent();
        }

        return Optional.fromNullable(compiledTemplates.iterator().next());
    }

    @Override
    public Collection<String> compileToJsSrc(Collection<URL> templates, @Nullable SoyMsgBundle soyMsgBundle) {
        Preconditions.checkNotNull("soyJsSrcOptions", soyJsSrcOptions);
        logger.debug("SoyJavaScript compilation of template:" + templates);
        final long time1 = System.currentTimeMillis();

        final SoyFileSet soyFileSet = buildSoyFileSetFrom(templates);

        final List<String> ret = soyFileSet.compileToJsSrc(soyJsSrcOptions, soyMsgBundle);

        final long time2 = System.currentTimeMillis();

        logger.debug("SoyJavaScript compilation complete." + (time2 - time1) + " ms");

        return ret;
    }

    private SoyFileSet buildSoyFileSetFrom(final Collection<URL> urls) {
        final SoyFileSet.Builder builder = new SoyFileSet.Builder();

        for (final URL url : urls) {
            builder.setAllowExternalCalls(true);
            builder.add(url);
        }

        addCompileTimeGlobalModel(builder);

        return builder.build();
    }

    public void setHotReloadMode(final boolean hotReloadMode) {
        this.hotReloadMode = hotReloadMode;
    }

    public void setCompileTimeGlobalModelResolver(final CompileTimeGlobalModelResolver compileTimeGlobalModelResolver) {
        this.compileTimeGlobalModelResolver = compileTimeGlobalModelResolver;
    }

    public void setSoyJsSrcOptions(final SoyJsSrcOptions soyJsSrcOptions) {
        this.soyJsSrcOptions = soyJsSrcOptions;
    }

    public boolean isHotReloadMode() {
        return hotReloadMode;
    }

    public boolean isHotReloadModeOff() {
        return !hotReloadMode;
    }

    public SoyJsSrcOptions getSoyJsSrcOptions() {
        return soyJsSrcOptions;
    }

    public SoyTofuOptions getSoyTofuOptions() {
        return soyTofuOptions;
    }

    public void setSoyTofuOptions(SoyTofuOptions soyTofuOptions) {
        this.soyTofuOptions = soyTofuOptions;
    }

} public interface TofuCompiler {

    /**
     * Obtains a binary compiled version based on a set of input urls
     * @param files - urls
     * @return soy tofu
     * @throws java.io.IOException - i/o error
     */
    SoyTofu compile(Collection<URL> files) throws IOException;

    /**
     * Obtains a compiled template to JavaScript as a String based on a template url
     * @param template - template
     * @param soyMsgBundle - bundle
     * @return js
     */
    Optional<String> compileToJsSrc(URL template, @Nullable SoyMsgBundle soyMsgBundle);

    /**
     * Obtains a compiled template to JavaScript as a String based on a collection of template urls
     * @param templates - templates
     * @param soyMsgBundle - bundle
     * @return js
     */
    Collection<String> compileToJsSrc(Collection<URL> templates, @Nullable SoyMsgBundle soyMsgBundle);

} public class DefaultToSoyDataConverter implements ToSoyDataConverter {
	private Matcher<PropertyDescriptor> ignorablePropertiesMatcher;

	public DefaultToSoyDataConverter() {
		setIgnorablePropertiesMatcher(new DefaultIgnorablePropertiesMatcher());
	}

	public void setIgnorablePropertiesMatcher(
			Matcher<PropertyDescriptor> ignorablePropertiesMatcher) {
		this.ignorablePropertiesMatcher = ignorablePropertiesMatcher;
	}

	@Override
	public Optional<SoyMapData> toSoyMap(final Object model) throws Exception {
		if (model == null) {
			return Optional.absent();
		}

		return Optional.fromNullable(objectToSoyDataMap(model));
	}

	@SuppressWarnings("unchecked")
	protected Map<String, ?> toSoyCompatibleMap(final Object obj)
			throws Exception {
		Object ret = toSoyCompatibleObjects(obj);
		if (!(ret instanceof Map)) {
			throw new IllegalArgumentException("Input should be a Map or POJO.");
		}

		return (Map<String, ?>) ret;
	}

	protected Object toSoyCompatibleObjects(Object obj) throws Exception {
		if (obj == null) {
			return obj;
		}

		if (Primitives.isWrapperType(obj.getClass()) || obj instanceof String) {
			return obj;
		}

		if(obj.getClass().isEnum()) {
			return ((Enum)obj).name();
		}

		if (obj instanceof Map) {
			@SuppressWarnings("unchecked")
			Map<String, Object> map = (Map<String, Object>) obj;
			Map<String, Object> newMap = new HashMap<String, Object>(map.size());
			for (String key : map.keySet()) {
				newMap.put(key, toSoyCompatibleObjects(map.get(key)));
			}
			return newMap;
		}

		if (obj instanceof Iterable<?>) {
			List<Object> list = Lists.newArrayList();
			for (Object subValue : ((Iterable<?>) obj)) {
				list.add(toSoyCompatibleObjects(subValue));
			}
			return list;
		}

		if (obj instanceof Callable) {
			final Callable<?> callable = (Callable<?>) obj;

			return toSoyCompatibleObjects(callable.call());
		}

		if (obj.getClass().isArray()) {
			return obj;
		}

		@SuppressWarnings("unchecked")
		Map<String, Object> pojoMap = (Map<String, Object>) pojoToMap(obj);
		Map<String, Object> newMap = new HashMap<String, Object>(pojoMap.size());
		for (String key : pojoMap.keySet()) {
			newMap.put(key, toSoyCompatibleObjects(pojoMap.get(key)));
		}

		return newMap;
	}

	protected Map<String, ?> pojoToMap(final Object pojo) {
		Map<String, Object> map = new HashMap<String, Object>();

		try {
			final BeanInfo beanInfo = Introspector.getBeanInfo(pojo.getClass());

			PropertyDescriptor[] propertyDescriptors = beanInfo
					.getPropertyDescriptors();
			for (PropertyDescriptor pd : propertyDescriptors) {
				if (!isIgnorable(pd)) {
					map.put(pd.getName(), pd.getReadMethod().invoke(pojo));
				}
			}

		} catch (Exception e) {
			throw new RuntimeException(e);
		}

		return map;
	}

	protected SoyMapData objectToSoyDataMap(Object obj) throws Exception {
		if (obj == null) {
			return new SoyMapData();
		}
		if (obj instanceof SoyMapData) {
			return (SoyMapData) obj;
		}
		return new SoyMapData(toSoyCompatibleMap(obj));
	}

	protected boolean isIgnorable(PropertyDescriptor pd) {
		return ignorablePropertiesMatcher.matches(pd);
	}

	private static class DefaultIgnorablePropertiesMatcher extends
			AbstractMatcher<PropertyDescriptor> implements Serializable {
		private static final long serialVersionUID = 0;
		private static final List<String> ignorableProperties = unmodifiableList(newArrayList(
				"class", "metaClass"));

		@Override
		public boolean matches(PropertyDescriptor pd) {
			return pd.getReadMethod() == null
					|| ignorableProperties.contains(pd.getName());
		}
	}
} public interface ToSoyDataConverter {

    /**
     * Convert a view model object to SoyMapData structure
     * @param model - model
     * @return maybe soy map data
     * @throws Exception - error to convert
     */
    Optional<SoyMapData> toSoyMap(@Nullable final Object model) throws Exception;

} public interface ModelAdjuster {

    @Nullable Object adjust(@Nullable Object obj);

} public class SpringModelAdjuster implements ModelAdjuster {

    private String modelKey = "model";

    @Override
    public @Nullable Object adjust(@Nullable final Object obj) {
        if (obj instanceof Model) {
            final Model model = (Model) obj;

            return model.asMap().get("model");
        }
        if (obj instanceof Map) {
            final Map map = (Map) obj;

            return map.get("model");
        }

        return obj;
    }

    public void setModelKey(final String modelKey) {
        this.modelKey = modelKey;
    }

    public String getModelKey() {
        return modelKey;
    }

} public interface CompileTimeGlobalModelResolver {

    Optional<SoyMapData> resolveData();

} public class EmptyCompileTimeGlobalModelResolver implements CompileTimeGlobalModelResolver {

    public Optional<SoyMapData> resolveData() {
        return Optional.absent();
    }

} public class EmptyGlobalRuntimeModelResolver implements GlobalRuntimeModelResolver {

    public Optional<SoyMapData> resolveData(final HttpServletRequest request, final HttpServletResponse response, final Map<String, ? extends Object> model) {
        return Optional.absent();
    }

} public interface GlobalRuntimeModelResolver {

    Optional<SoyMapData> resolveData(HttpServletRequest request, HttpServletResponse response, Map<String, ? extends Object> model);

} public interface CompiledTemplatesHolder {

    Optional<SoyTofu> compiledTemplates() throws IOException;

} public class DefaultCompiledTemplatesHolder implements InitializingBean, CompiledTemplatesHolder {

    private static final Logger logger = LoggerFactory.getLogger(DefaultCompiledTemplatesHolder.class);

    private boolean hotReloadMode = SoyViewConfigDefaults.DEFAULT_HOT_RELOAD_MODE;

    private TofuCompiler tofuCompiler = new EmptyTofuCompiler();

    private TemplateFilesResolver templatesFileResolver = new EmptyTemplateFilesResolver();

    private Optional<SoyTofu> compiledTemplates = Optional.absent();

    private boolean preCompileTemplates = SoyViewConfigDefaults.DEFAULT_PRECOMPILE_TEMPLATES;

    public Optional<SoyTofu> compiledTemplates() throws IOException {
        if (shouldCompileTemplates()) {
            this.compiledTemplates = Optional.fromNullable(compileTemplates());
        }

        return compiledTemplates;
    }

    private boolean shouldCompileTemplates() {
        return isHotReloadMode() || !compiledTemplates.isPresent();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        logger.debug("TemplatesHolder init...");
        if (preCompileTemplates) {
            this.compiledTemplates = Optional.fromNullable(compileTemplates());
        }
    }

    private SoyTofu compileTemplates() throws IOException {
        Preconditions.checkNotNull(templatesFileResolver, "templatesRenderer cannot be null!");
        Preconditions.checkNotNull(tofuCompiler, "tofuCompiler cannot be null!");

        final Collection<URL> templateFiles = templatesFileResolver.resolve();
        if (templateFiles != null && templateFiles.size() > 0) {
            logger.debug("Compiling templates, no:{}", templateFiles.size());

            return tofuCompiler.compile(templateFiles);
        }

        throw new IOException("0 template files have been found, check your templateFilesResolver!");
    }

    public void setHotReloadMode(final boolean hotReloadMode) {
        this.hotReloadMode = hotReloadMode;
    }

    public boolean isHotReloadMode() {
        return hotReloadMode;
    }

    public boolean isHotReloadModeOff() {
        return !hotReloadMode;
    }

    public void setTofuCompiler(TofuCompiler tofuCompiler) {
        this.tofuCompiler = tofuCompiler;
    }

    public void setTemplatesFileResolver(TemplateFilesResolver templatesFileResolver) {
        this.templatesFileResolver = templatesFileResolver;
    }

    public void setPreCompileTemplates(boolean preCompileTemplates) {
        this.preCompileTemplates = preCompileTemplates;
    }

} public interface LocaleProvider {

    Optional<Locale> resolveLocale(HttpServletRequest request);

} public class SpringLocaleProvider implements LocaleProvider {

    @Override
    public Optional<Locale> resolveLocale(final HttpServletRequest request) {
        final LocaleResolver localeResolver = (LocaleResolver) request.getAttribute(DispatcherServlet.LOCALE_RESOLVER_ATTRIBUTE);
        if (localeResolver != null) {
            final Locale locale = localeResolver.resolveLocale(request);
            if (locale != null) {
                return Optional.of(locale);
            }
        }

        return Optional.fromNullable(request.getLocale());
    }

} public class DefaultTemplateRenderer implements TemplateRenderer {

    private static final Logger logger = LoggerFactory.getLogger(DefaultTemplateRenderer.class);


    protected ToSoyDataConverter toSoyDataConverter = new DefaultToSoyDataConverter();

    /**
     * whether debug is on, in case it is on - Soy's Renderer Don't Add To Cache will be turned on, which means
     * renderer caching will be disabled */
    private boolean hotReloadMode = SoyViewConfigDefaults.DEFAULT_HOT_RELOAD_MODE;

    @Override
    public void render(final RenderRequest renderRequest) throws Exception {
        if (!renderRequest.getCompiledTemplates().isPresent()) {
            logger.warn("compiled templates are not present, nothing to render!");
            return;
        }

        final SoyTofu compiledTemplates = renderRequest.getCompiledTemplates().get();

        final String templateName = renderRequest.getTemplateName();
        final SoyTofu.Renderer renderer = compiledTemplates.newRenderer(templateName);

        final Optional<SoyMapData> soyModel = toSoyDataConverter.toSoyMap(renderRequest.getModel());
        setupRenderer(renderer, renderRequest, soyModel);
        writeResponse(renderer, renderRequest);
    }

    protected void setupRenderer(final SoyTofu.Renderer renderer, final RenderRequest renderRequest, final Optional<SoyMapData> model) throws Exception {
        if (model.isPresent()) {
            renderer.setData(model.get());
        }
        final Optional<SoyMapData> globalModel = renderRequest.getGlobalRuntimeModel();
        if (globalModel.isPresent()) {
            renderer.setIjData(globalModel.get());
        }
        final Optional<SoyMsgBundle> soyMsgBundleOptional = renderRequest.getSoyMsgBundle();
        if (soyMsgBundleOptional.isPresent()) {
            renderer.setMsgBundle(soyMsgBundleOptional.get());
            if (isHotReloadModeOff()) {
                if (renderRequest.getCompiledTemplates().isPresent()) {
                    renderRequest.getCompiledTemplates().get().addToCache(soyMsgBundleOptional.get(), null);
                }
            }
        }
        if (isHotReloadMode()) {
            renderer.setDontAddToCache(true);
        }
    }

    protected void writeResponse(final SoyTofu.Renderer renderer, final RenderRequest renderRequest) throws IOException {
        final HttpServletResponse response = renderRequest.getResponse();
        final SoyView soyView = renderRequest.getSoyView();

        response.setContentType(soyView.getContentType());

        final PrintWriter writer = response.getWriter();
        writer.write(renderer.render());
        writer.flush();
        response.flushBuffer();
    }

    public void setToSoyDataConverter(final ToSoyDataConverter toSoyDataConverter) {
        this.toSoyDataConverter = toSoyDataConverter;
    }

    public void setHotReloadMode(boolean hotReloadMode) {
        this.hotReloadMode = hotReloadMode;
    }

    public boolean isHotReloadMode() {
        return hotReloadMode;
    }

    public boolean isHotReloadModeOff() {
        return !hotReloadMode;
    }

} public interface TemplateRenderer {

    void render(RenderRequest renderRequest) throws Exception;

} @ParametersAreNonnullByDefault
@ThreadSafe
public class DefaultTemplateFilesResolver implements TemplateFilesResolver, ServletContextAware, InitializingBean {

    private static final Logger logger = LoggerFactory.getLogger(DefaultTemplateFilesResolver.class);

    /** spring resource that points to a root path, in which soy templates are located */
    private Resource templatesLocation = null;

    private boolean recursive = true;

    private boolean hotReloadMode = SoyViewConfigDefaults.DEFAULT_HOT_RELOAD_MODE;

    /** a thread safe cache for resolved templates, no need to worry of ddos attack */
    /** friendly */ CopyOnWriteArrayList<URL> cachedFiles = new CopyOnWriteArrayList<URL>();

    private String filesExtension = SoyViewConfigDefaults.DEFAULT_FILES_EXTENSION;

    @Inject
    private ServletContext servletContext;

    public DefaultTemplateFilesResolver() {
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        if (templatesLocation == null) {
            templatesLocation = new ServletContextResource(servletContext, SoyViewConfigDefaults.DEFAULT_TEMPLATE_FILES_PATH);
        }
    }

    @Override
    public Collection<URL> resolve() throws IOException {
        Preconditions.checkNotNull(templatesLocation, "templatesLocation cannot be null!");

        if (hotReloadMode) {
            final List<URL> files = toFiles(templatesLocation);
            logger.debug("Debug on - resolved files:" + files.size());

            return files;
        }

        //no debug
        synchronized (cachedFiles) {
            if (cachedFiles.isEmpty()) {
                final List<URL> files = toFiles(templatesLocation);
                logger.debug("templates location:" + templatesLocation);
                logger.debug("Using cache resolve, debug off, urls:" + files.size());
                cachedFiles.addAll(files);
            }
        }

        return cachedFiles;
    }

    @Override
    public Optional<URL> resolve(final @Nullable String templateFileName) throws IOException {
        if (templateFileName == null) {
            return Optional.absent();
        }

        final Collection<URL> files = resolve();

        final URL templateFile = Iterables.find(files, new Predicate<URL>() {

            @Override
            public boolean apply(final URL url) {
                final String fileName = url.getFile();
                final File file = new File(fileName);

                return file.toURI().toString().endsWith(normalizeTemplateName(templateFileName));
            }

        }, null);

        return Optional.fromNullable(templateFile);
    }

    private String normalizeTemplateName(final String templateFileName) {
        String normalizedTemplateName = templateFileName;
        if (!templateFileName.endsWith(dotWithExtension())) {
            normalizedTemplateName = templateFileName + dotWithExtension();
        }

        return normalizedTemplateName;
    }

    private List<URL> toFiles(final Resource templatesLocation) {
        final List<URL> templateFiles = Lists.newArrayList();
        try {
            File baseDirectory = templatesLocation.getFile();
            if (baseDirectory.isDirectory()) {
                templateFiles.addAll(findSoyFiles(baseDirectory, recursive));
            } else {
                throw new IllegalArgumentException("Soy template base directory:" + templatesLocation + "' is not a directory");
            }
        } catch (final IOException e) {
            throw new IllegalArgumentException("Soy template base directory '" + templatesLocation + "' does not exist", e);
        }

        return templateFiles;
    }

    protected List<URL> findSoyFiles(final File baseDirectory, final boolean recursive) throws MalformedURLException {
        final List<URL> soyFiles = new ArrayList<URL>();
        findSoyFiles(soyFiles, baseDirectory, recursive);

        return soyFiles;
    }

    protected void findSoyFiles(final List<URL> soyFiles, final File baseDirectory, final boolean recursive) throws MalformedURLException {
        final File[] files = baseDirectory.listFiles();
        if (files != null) {
            for (final File file : files) {
                if (file.isFile()) {
                    if (file.getName().endsWith(dotWithExtension())) {
                        soyFiles.add(file.toURI().toURL());
                    }
                } else if (file.isDirectory() && recursive) {
                    findSoyFiles(soyFiles, file, recursive);
                }
            }
        } else {
            throw new IllegalArgumentException("Unable to retrieve contents of:" + baseDirectory);
        }
    }

    private String dotWithExtension() {
        return "." + filesExtension;
    }

    public void setTemplatesLocation(Resource templatesLocation) {
        this.templatesLocation = templatesLocation;
    }

    public void setRecursive(boolean recursive) {
        this.recursive = recursive;
    }

    public void setHotReloadMode(boolean hotReloadMode) {
        this.hotReloadMode = hotReloadMode;
    }

    public Resource getTemplatesLocation() {
        return templatesLocation;
    }

    public boolean isRecursive() {
        return recursive;
    }

    public boolean isHotReloadMode() {
        return hotReloadMode;
    }

    public String getFilesExtension() {
        return filesExtension;
    }

    public void setFilesExtension(String filesExtension) {
        this.filesExtension = filesExtension;
    }

    @Override
    public void setServletContext(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

} public interface TemplateFilesResolver {

    /**
     * Iterate over all files and provide urls of files pointing to soy template files
     * @return urls
     * @throws java.io.IOException - i/o error
     */
    Collection<URL> resolve() throws IOException;

    /**
     * Iterate over all files and provide a matching url for a template passed in as a parameter
     * @param templateName - name of template
     * @return urls
     * @throws java.io.IOException - i/o error
     */
    Optional<URL> resolve(String templateName) throws IOException;

}
import com.google.template.soy.jssrc.SoyJsSrcOptions;
import com.google.template.soy.tofu.SoyTofuOptions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.support.ServletContextResource;
import org.springframework.web.servlet.ViewResolver;
import pl.matisoft.soy.ContentNegotiator;
import pl.matisoft.soy.DefaultContentNegotiator;
import pl.matisoft.soy.SoyTemplateViewResolver;
import pl.matisoft.soy.bundle.DefaultSoyMsgBundleResolver;
import pl.matisoft.soy.bundle.SoyMsgBundleResolver;
import pl.matisoft.soy.compile.DefaultTofuCompiler;
import pl.matisoft.soy.compile.TofuCompiler;
import pl.matisoft.soy.data.DefaultToSoyDataConverter;
import pl.matisoft.soy.data.ToSoyDataConverter;
import pl.matisoft.soy.data.adjust.ModelAdjuster;
import pl.matisoft.soy.data.adjust.SpringModelAdjuster;
import pl.matisoft.soy.global.compile.CompileTimeGlobalModelResolver;
import pl.matisoft.soy.global.compile.EmptyCompileTimeGlobalModelResolver;
import pl.matisoft.soy.global.runtime.EmptyGlobalRuntimeModelResolver;
import pl.matisoft.soy.global.runtime.GlobalRuntimeModelResolver;
import pl.matisoft.soy.holder.CompiledTemplatesHolder;
import pl.matisoft.soy.holder.DefaultCompiledTemplatesHolder;
import pl.matisoft.soy.locale.LocaleProvider;
import pl.matisoft.soy.locale.SpringLocaleProvider;
import pl.matisoft.soy.render.DefaultTemplateRenderer;
import pl.matisoft.soy.render.TemplateRenderer;
import pl.matisoft.soy.template.DefaultTemplateFilesResolver;
import pl.matisoft.soy.template.TemplateFilesResolver;
import javax.inject.Inject;
import javax.servlet.ServletContext;
package pl.matisoft.soy.config;



/**
 * Created with IntelliJ IDEA.
 * User: mati
 * Date: 12/11/2013
 * Time: 19:55
 */
@Configuration
public class SpringSoyViewBaseConfig {

    @Value("${soy.hot.reload.mode:false}")
    private boolean hotReloadMode;

    @Value("${soy.templates.resolve.recursively:true}")
    private boolean recursive;

    @Value("${soy.templates.file.extension:soy}")
    private String fileExtension;

    @Value("${soy.templates.directory:/WEB-INF/templates}")
    private String templatesPath;

    @Value("${soy.i18n.xliff.path:xliffs/messages}")
    private String messagesPath;

    @Value("${soy.encoding:utf-8}")
    private String encoding;

    @Value("${soy.i18n.fallback.to.english:true}")
    private boolean fallbackToEnglish;

    @Value("${soy.preCompile.templates:false}")
    private boolean preCompileTemplates;

    @Value("${soy.indexView:index}")
    private String indexView;

    @Value("${soy.logical.prefix:soy:}")
    private String logicalPrefix;

    @Value("${soy.resolver.order:2147483647}")
    private int order;

    @Inject
    private ServletContext servletContext;

    @Bean
    public LocaleProvider soyLocaleProvider() {
        return new SpringLocaleProvider();
    }

    @Bean
    public DefaultTemplateFilesResolver soyTemplateFilesResolver() throws Exception {
        final DefaultTemplateFilesResolver defaultTemplateFilesResolver = new DefaultTemplateFilesResolver();
        defaultTemplateFilesResolver.setHotReloadMode(hotReloadMode);
        defaultTemplateFilesResolver.setRecursive(recursive);
        defaultTemplateFilesResolver.setFilesExtension(fileExtension);
        defaultTemplateFilesResolver.setTemplatesLocation(new ServletContextResource(servletContext, templatesPath));

        return defaultTemplateFilesResolver;
    }

    @Bean
    public CompileTimeGlobalModelResolver soyCompileTimeGlobalModelResolver() {
        return new EmptyCompileTimeGlobalModelResolver();
    }

    @Bean
    public ToSoyDataConverter soyToSoyDataConverter() {
        return new DefaultToSoyDataConverter();
    }

    @Bean
    public SoyJsSrcOptions soyJsSourceOptions() {
        return new SoyJsSrcOptions();
    }

    @Bean
    public SoyTofuOptions soyTofuOptions() {
        final SoyTofuOptions soyTofuOptions = new SoyTofuOptions();
        soyTofuOptions.setUseCaching(!hotReloadMode);

        return soyTofuOptions;
    }

    @Bean
    public TofuCompiler soyTofuCompiler(final CompileTimeGlobalModelResolver compileTimeGlobalModelResolver, final SoyJsSrcOptions soyJsSrcOptions, final SoyTofuOptions soyTofuOptions) {
        final DefaultTofuCompiler defaultTofuCompiler = new DefaultTofuCompiler();
        defaultTofuCompiler.setHotReloadMode(hotReloadMode);
        defaultTofuCompiler.setCompileTimeGlobalModelResolver(compileTimeGlobalModelResolver);
        defaultTofuCompiler.setSoyJsSrcOptions(soyJsSrcOptions);
        defaultTofuCompiler.setSoyTofuOptions(soyTofuOptions);

        return defaultTofuCompiler;
    }

    @Bean
    public SoyMsgBundleResolver soyMsgBundleResolver() {
        final DefaultSoyMsgBundleResolver defaultSoyMsgBundleResolver = new DefaultSoyMsgBundleResolver();
        defaultSoyMsgBundleResolver.setHotReloadMode(hotReloadMode);
        defaultSoyMsgBundleResolver.setMessagesPath(messagesPath);
        defaultSoyMsgBundleResolver.setFallbackToEnglish(fallbackToEnglish);

        return defaultSoyMsgBundleResolver;
    }

    @Bean
    public CompiledTemplatesHolder soyTemplatesHolder(final TemplateFilesResolver templateFilesResolver, final TofuCompiler tofuCompiler) throws Exception {
        final DefaultCompiledTemplatesHolder defaultCompiledTemplatesHolder = new DefaultCompiledTemplatesHolder();
        defaultCompiledTemplatesHolder.setHotReloadMode(hotReloadMode);
        defaultCompiledTemplatesHolder.setPreCompileTemplates(preCompileTemplates);
        defaultCompiledTemplatesHolder.setTemplatesFileResolver(templateFilesResolver);
        defaultCompiledTemplatesHolder.setTofuCompiler(tofuCompiler);

        return defaultCompiledTemplatesHolder;
    }

    @Bean
    public TemplateRenderer soyTemplateRenderer(final ToSoyDataConverter toSoyDataConverter) {
        final DefaultTemplateRenderer defaultTemplateRenderer = new DefaultTemplateRenderer();
        defaultTemplateRenderer.setHotReloadMode(hotReloadMode);
        defaultTemplateRenderer.setToSoyDataConverter(toSoyDataConverter);

        return defaultTemplateRenderer;
    }

    @Bean
    public ModelAdjuster soySpringModelAdjuster() {
        return new SpringModelAdjuster();