text:
layout: post
title: "REST lesson learned: Avoid hackable URLs"
description: "Avoid hackable URLs if you are building a REST API."
date: 2013-05-01 10:29 UTC
tags: [Software Design, Services, REST]
image: "/content/binary/great-divide-between-richardson-level-2-and-3.png"
---
{% include JB/setup %}

		{{ page.description }}

		This is a lesson about REST API design that I learned while building non-trivial REST APIs. If you provide a full-on level 3 REST API, consider avoiding hackable URLs.

		Hackable URLs #

		A hackable URL is a URL where there's a clear pattern or template for constructing the URL. As an example, if I present to you the URL http://foo.ploeh.dk/products/1234, it's easy to guess that this is a resource representing a product with the SKU of 1234. If you know the SKU of another product, it's easy to 'hack' the URL to produce e.g. http://foo.ploeh.dk/products/5678.

		That's a really nice feature of your API if you are doing a level 1 or 2 API, but for a HATEOAS API, this defies the purpose.

		The great divide #

		Please notice that the shift from level 2 to level 3 RESTful APIs mark a fundamental shift in the way you should approach URL design.

		Hackable URLs are great for level 1 and 2 APIs because the way you (as a client) are told to construct URLs is by assembling them from templates. As an example, the Windows Azure REST APIs explicitly instruct you to construct the URL in a particular way: the URL to get BLOB container properties is https://myaccount.blob.core.windows.net/mycontainer?restype=container, where you should replace myaccount with your account name, and mycontainer with your container name. While code aesthetics are subjective, it's not even a particularly clean URL, but it's easy enough to produce. The URL template is part of the contract, which puts the Windows Azure API solidly at level 2 of the Richardson Maturity Model. If I were designing a level 1 or 2 API, I'd make sure to make URLs hackable, too.

		However, if you're building a level 3 API, hypermedia is king. Clients are expected to follow links. The addresses of resources are not published as having a particular template; instead, clients must follow semantic links in order to arrive at the desired resource(s). When hypermedia is the engine of application state, it's no good if the client can short-circuit the application flow by 'hacking' URLs. It may leave the application in an inconsistent state if it tries to do that.

		Hackable URLs are great for level 1 and 2 APIs, but counter-productive for level 3 APIs.

		Evolving URLs #

		One of the main attractions of building a level 3 RESTful API is that it's easier to evolve. Exactly because URL templates are not part of the contract, you can decide to change the URL structure when evolving your API.

		Imagine that the first version of your API has an (internal) URL template like /orders/{customerId}, so that the example URL http://foo.ploeh.dk/orders/1234 is the address of the order history for customer 1234. However, in version 2 of your API, you realize that this way of thinking is still too RPC-like, and you'd rather prefer /customers/{customerId}/orders, e.g. http://foo.ploeh.dk/customers/1234/orders.

		With a level 3 RESTful API, you can change your internal URL templates, and as long as you keep providing links, clients following links will not notice the difference. However, if clients are 'hacking' your URLs, their applications may stop working if you change URL templates.

		Keep clients safe #

		In the end, HATEOAS is about encapsulation: make it easy for the client to do the right thing, and make it hard for the client to do the wrong thing. Following links will make clients more robust, because they will be able to handle changes in the API. Making it easy for clients to follow links is one side of designing a good API, but the other side is important too: make it difficult for clients to not follow links: make it difficult for clients to 'hack' URLs.

		The services I've helped design so far are level 3 APIs, but they still used hackable URLs. One reason for that was that this is the default in the implementation platform we used (ASP.NET Web API); another reason was that I thought it would be easier for me and the rest of the development team if the URLs were human-readable. Today, I think this decision was a mistake.

		What's the harm of supplying human-readable URLs for a level 3 RESTful API? After all, if a client only follows links, the values of the URLs shouldn't matter.

		Indeed, if the client only follows links. However, clients are created by human developers, and humans often take the road of least resistance. While there are long-time benefits (robustness) from following links, it is more work in the short term. The API team and I repeatedly experienced that the developers consuming our APIs had 'hacked' our URLs; when we changed our URL templates, their clients broke and they complained. Even though we had tried to explicitly tell them that they must follow links, they didn't. While we never documented our URL templates, they were simply too easy to guess from pure extrapolation.

		Opaque URLs #

		In the future, I plan to make URLs opaque when building level 3 APIs. Instead of http://foo.ploeh.dk/customers/1234/orders, I'm going to make it http://foo.ploeh.dk/DC884298C70C41798ABE9052DC69CAEE, and instead of http://foo.ploeh.dk/products/2345, I'm going to make it http://foo.ploeh.dk/598CB0CAC30646E1BB768596BFE91F2C, and so on.

		Obviously, that means that my API will have to maintain some sort of two-way lookup table that can map DC884298C70C41798ABE9052DC69CAEE to a request for customer 1234's orders, 598CB0CAC30646E1BB768596BFE91F2C to request for product 2345, but that's trivial to implement.

		It puts a small burden on the server(s), but effectively stops client developers from shooting themselves in their feet.

		Summary #

		Hackable URLs are a good idea if you are building a web site, or a level 1 or 2 REST API, but unless you know that all client developers are enthusiastic RESTafarians, consider producing opaque URLs for level 3 REST APIs.

Comments

			Jon Curtis

				I understand the benefits of the idea that clients must follow links, I just can't see how it would work in reality. Say you are building a website to display products and you are consuming a rest API, how do you support deep linking to individual products? OK, you can either use the same URL as the underlying API or encode it in, but what are you expected to do if the API changes it's links? Redirect the user to your home page? What if you want to display information from 2 or more resources on the same webpage?

2013-05-15 8:04 UTC

			Mark Seemann

				First of all, keep in mind that while it can be helpful to think about REST design principles in terms of "how would I design this if it was a web site", a REST API is not a web site.

				You can do deep linking in your web site, but why would you want to do 'deep linking' for an API? These are two different concerns.

				It's very common to create a web site where each page calls many individual services. This can be done either from the web server (e.g. from ASP.NET or similar), or from the browser as AJAX calls. This is commonly known as mash-up architecture, because the GUI is really just a mash-up of service data. Amazon.com works that way. You can still deep link to a web page; it's the web page's responsibility to figure out which services to call with what parameters.

				That said, as described in the RESTful Web Services Cookbook, you should serve cool URLs, so if you ever decide to change your internal URI template, you should leave a 301 (Moved Permanently) behind at the old URL. This would enable a client that once bookmarked a resource to follow the redirect to the new address.

2013-05-15 9:11 UTC

			Jon Curtis

				When you say "it's the web page's responsibility to figure out which services to call", that's what I'm getting at, how would the client do that?

				I'm guessing one way would be to cache links followed and update them on 301s or "re-follow" on 404s. So, say you wanted a web page displaying "product/24", you might:

GET the REST endpoint

GET product catalogue URL from response

GET product URL from response

Cache the product URL against the website product URL

Subsequent requests hit the cached URL

				Then if the product URL changes, if the response is 301, you simply update the cache. If the response is 404 then you'd redo the above steps.

				I'm just thinking this through, is the above a "standard" approach for creating rest clients?

2013-05-15 12:04 UTC

			Mark Seemann

				That sounds like one way of doing it. I don't think there's a 'standard' way for creating RESTful clients.

				The sketch you paint sounds like a lot of work, and it seems that it would be easier if the client could simply assemble appropriate URLs from templates. That would indicate level 1 and 2 REST APIs, which might be perfectly fine if the only purpose of building the service is to support a GUI. However, what you get in exchange for the extra effort it takes to consume a level 3 API, is better decoupling. It's always going to be a trade-off.

				It's definitely going to be more work to build and consume a truly HATEOAS-based API, so you should only do it if it's going to provide a good return on investment. When would that be? One general scenario I can think of is when you're building a service, which is going to be consumed by multiple (unknown) clients. If you control the service, but not the clients, I'd say a level 3 API is very beneficial, because it enables you to evolve the API independently of the clients. Conversely, if the only purpose of building a service is to support a single client, it's probably going to be overkill.

2013-05-15 15:15 UTC

			Reda Bendiar

				The way I see it is that being HATEOAS compliant does not impose non-hackable URLs, but that URLs - even if they might look hackable (/1, /2...etc) - are not guaranteed to work, are not part of the contract and thus should not be relied uppon. In other words, a link is only guaranteed to work if you, the client, got it from a previous reponse, be it "hackable-looking" or not.

				So I think opaque URLs are just a way to inforce that contract. But don't we always here that "a good REST client should be well behaved" ?

2014-10-10 11:11 UTC

			Mark Seemann

				Reda, thank you for writing. You're right, and if we could rely on all clients to be well-behaved, there'd be no problems. However, in my experience, client developers often don't read the documentation particularly thoroughly. Instead, they look at the returned data and start inferring the URL scheme from examples. I already wrote about this in this post:

"The API team and I repeatedly experienced that the developers consuming our APIs had 'hacked' our URLs; when we changed our URL templates, their clients broke and they complained. Even though we had tried to explicitly tell them that they must follow links, they didn't."

				So, in this imperfect world, non-hackable URLs start to look attractive, because then the client developers have no choice but to follow the links.

2014-10-10 12:02 UTC

			Kijana Woodard

				I'd like to propose an alternate strategy to achieve the desired result while maintaining
				the ability to easily debug production issues at the client:
				only use the non-hackable urls for the "dev sandbox".

				Another inducement to better client behvior might be to have the getting started documentation
				use existing libraries for the chosen hypermedia media type.

				Finally, have the clients send a "developer [org] identifier". Find a way to probe clients for
				correctness. For instance, occasionally alter hrefs, but also support the "hackable" form.

				With this data, be proactive about informing the user that
				"we're planning a release and your app will probably break because
				you're not following links as expected".

				In the end, some people will still fail and be angry. With opaque urls, I would worry that users
				would have a hard time communicating production issues when looking at logs /fiddler / browser tools / etc.

2014-12-23 17:40 UTC

Mark Seemann

				Kijana, thank you for writing. You suggest various good ideas that I'll have to keep in mind. Not all of them are universally applicable, but then I also don't think that my suggestion should be applied indiscriminately.

				Your first suggestion assumes that there is a dev sandbox; this may or may not be the case. The APIs with which I currently work have no dev sandboxes.

				The idea about using existing client libraries for the chosen hypermedia type is only possible if such libraries exist. The APIs I currently design use vendor media types, so client libraries only exist if we develop them ourselves. However, one of the important goals of RESTful services is to ensure interoperability, so we can't assume that clients are going to run on .NET, or Java, or Ruby, or whatever. For vendor media types, I don't think this is a viable option.

				Using a client identifier is an option, but in order to work well, there must be some way to correlate that identifier to a contact in the client's organisation. That's an option if you already have a mechanism in place where you only allow known clients to access your API. On the other hand, if you publish a truly scalable public API, you may not want to do that, as registration requirements tend to hurt adoption. The APIs I currently work with is a mix of both of those; some are publicly accessible, while others require a 'client key'.

				These are all interesting ideas, but ultimately, I'm not sure I understand your concern. Let's first establish that 'users' are other programmers. Why would a URL like http://foo.ploeh.dk/DC884298C70C41798ABE9052DC69CAEE be harder to communicate than http://foo.ploeh.dk/customers/1234/orders? Isn't it copy and paste in both cases?

				To be clear: I don't claim that obfuscated URLs don't make client developers' work more difficult; it does, compared to 'cheating' by hacking the URL schemes, instead of following links. Even for well-behaved client developers, another level of abstraction always makes things harder. On the other hand, this should also make clients more robust, so as always, it's a trade-off.

2015-01-22 08:55 UTC

Dan Kubb

“The API team and I repeatedly experienced that the developers consuming our APIs had 'hacked' our URLs; when we changed our URL templates, their clients broke and they complained. Even though we had tried to explicitly tell them that they must follow links, they didn't.”

				I recently worked on an API for an iOS app and we ran into the same problem. It didn't matter how often I said "these URLs are probably going to change, don't hard-code them into the app" we still ran into issues. This was an internal team, I can only imagine how much more difficult it would be with an external team.

				Instead of hashing the URLs, we changed the server-side URL generation to HMAC the URL and append the signature onto the query string. Requests without a valid signature would return a 403 Forbidden response. The root of the domain is the only URL that doesn't require a signature, and it returns a json response with signed URLs for each of the "base" resources.

				We enabled this on a staging server and since all dev was happening on it, all the hard-coded URLs stopped functioning and were quickly removed from the application. It forced both us and the iOS developers to think more about navigation between resources, since it was impossible to get from one resource to another without valid URLs included in the json responses. I think it was probably also nicer for debugging than hashed URLs would be.

2015-04-11 09:26 UTC

Mark Seemann

				Dan, thank you for sharing that great idea! That looks like a much better solution than my original suggestion of obscuring the URL, because the URL is still human-readable, and thus probably still easier to troubleshoot for developers.

				Your suggestion also doesn't require a lookup table. My suggested solution would require a lookup table in order to understand what each obscured URL actually means, and if you're running on multiple servers, that lookup table would have to be kept consistent across all servers, which can be hard to do in itself (you can use a database, but then you'd have a single point of failure). Your solution doesn't need any of that; it only requires that the HMAC key is the same on all servers.

				The only disadvantage that I can think of is that you may need to keep that HMAC key secret, particularly in internal projects, in order to prevent clients from (literally) hacking the URLs.

				Still, it sounds like your solution has more advantages, so I'm going to try that approach next time. Thank you for sharing!

2015-04-11 10:21 UTC

Dan Kubb

“The only disadvantage that I can think of is that you may need to keep that HMAC key secret, particularly in internal projects, in order to prevent clients from (literally) hacking the URLs.”

				That was one concern of ours too. One approach we considered was including the git commit SHA as part of the HMAC secret so that every commit would invalidate existing URLs. We decided against it because we were doing Continous Delivery, so multiple times a day we were deploying to the server, and we didn't want the communication overhead of having to notify everyone on each commit. We wanted to be able to merge feature branches into master and just know the CI server was going to handle deployment; and that unless we got alerts from CI, Pingdom or New Relic, everything is working as expected. The last thing we wanted to do was babysit the build so we could tell everyone "ok, the new build has deployed to staging, URLs will break so restart any clients you are in the middle of testing". Also part of the reason we use REST (and json-api) was to decouple front and backend development, it seemed counterproductive to introduce coupling back into the process.

				An internal team could still re-implement this whole mechanism and sign their own URLs. We could either generate random secrets every day or have a simple tool that we can use to change the secret at-will (probably both).

				Even though it doesn't completely stop URL hard coding, another idea we had was to include an expiration timestamp in the query string and then HMAC the URL with the timestamp included. We would use the responses' Cache-Control max-age to calculate the time, which would allow the client to use any URLs from cached responses. If the client didn't implement proper cache invalidation the URLs would inexplicably break, thus having a nice side-effect of forcing the client to handle Cache-Control max-age properly.

2015-04-11 17:58 UTC

Mark Seemann

				Dan, there are lots of interesting ideas there. What I assumed from your first comment is that you'd calculate the HMAC using asymmetric encryption, which would mean that unless clients have the server's encryption key, they wouldn't be able to recalculate the HMAC, and that would effectively prevent them from attempting to 'guess' URLs instead of following links.

				If an external client has the server's encryption key, you have a different sort of problem.

				However, for internal clients, developers may actually be able to find the key in your source control repository, build server, or wherever else you keep it (depending on the size of your organisation). You can solve this with security measures, like ACL-based security on the crypto key itself. It not hard, but it's something you may explicitly have to do.

				When it comes to tying the URL to cache invalidation, that makes me a bit uneasy. While RESTful clients should follow links, they are allowed to bookmark links. It's a fundamental tenet of proper web design (not only REST) that cool URIs don't change. A client should be allowed to keep a particular URL around forever, and a service following Postel's law should keep honouring requests for that URL. As the RESTful Web Services Cookbook explains, if you move the resource, you should at least return a 301 (Moved Permanently) "or, in rare cases," issue a 410 (Gone) response.

2015-04-12 11:35 UTC

Sandro Magi

As Dan said, you can use an HMAC to protect certain parameters in a URL from tampering. I actually formalized this approach in a .NET library I call Clavis, and which I first discussed in detail here.

Clavis just provides a simple framework for declaring a resources parameter interface. Given that interface, Clavis provides a way to generate URLs when given a valid set of parameters, and also provides a way to safely parse values given a constructed URL. Arbitrary types can be specified as resource parameters, and there's very little boilerplate to convert to/from strings.

Parameters are protected from tampering by default, but you can declare that some are unprotected, meaning the client can change them without triggering a server validation error. You want this in some cases, for instance to support GET forms, like a search.

2015-04-12 12:16 UTC

Ruben Verborgh

Late to the party, but I wonder how you will create a hypermedia form that generates http://foo.ploeh.dk/598CB0CAC30646E1BB768596BFE91F2C rather than http://foo.ploeh.dk/products/2345.

In the case of hyperlinks, I fully agree: linking to a product should not require an specifically structured identifier. However, hypermedia is more than links alone: what if the client wants to navigate to product with a given tracking number, or a list of people with a given first name?

"Hackable URIs" are not a necessity for REST, but they are a potential/likely consequence of hypermedia forms. Such forms can be described with less expressive power if only simple string replacements are required instead of more complex transformations.

2016-02-25 11:20 UTC

Mark Seemann

				Ruben, thank you for writing. This is a commonly occuring requirement, particularly when searching for particular resources, and I usually solve it with URI templates, as outlined in the RESTful Web Services Cookbook. Essentially, it involves providing URI templates like http://foo.ploeh.dk/0D89C19343DA4ED985A492DA1A8CDC53/{term}, and making sure that each template is served like a link, with a particular relationship type. For example:

<home xmlns="http://fnaah.ploeh.dk/productcatalog/2013/05">
  <link-template href="http://foo.ploeh.dk/0D89C19343DA4ED985A492DA1A8CDC53/{term}"
                 rel="http://catalog.api.ploeh.dk/docs/rels/product/search" />
</home>

				It isn't perfect, but the best I've been able to come up with, and it works fairly well in practice.

2016-03-02 19:25 UTC

Ruben Verborgh

				Sure, URI templates are a means for creating hypermedia controls. But the point is that what they generate are hackable URIs.

				You still have a (semi-)hackable URI with http://foo.ploeh.dk/0D89C19343DA4ED985A492DA1A8CDC53/{term}. Even though the /0D89C19343DA4ED985A492DA1A8CDC53/ part is non-manipulable, terms can still be changed at the end. This shows that hackable URIs cannot entirely be avoided if we want to keep hypermedia controls simple. What else are hackable URIs but underspecified, out-of-band hypermedia controls?

				An in-band IRI template makes the message follow the REST style by putting the control inside of the message, but it results in a hackable IRI. So, apparently, an HTTP REST API without hackable URIs is hard when there's more than just links.

2016-03-02 19:45 UTC

Mark Seemann

				Ruben, it's not that you can't avoid hackable URLs. URI templates are an optimisation that you can opt to apply, but you don't have to use them. As with so many other architecture decisions, it's a trade-off.

				When considering RESTful API design, it often helps to ask yourself: how would a web site running in a browser work? You don't have to copy web site mechanics one-to-one (for one, browsers utilise only GET and POST), but you can often get inspiration.

				A feature like search is, in my experience, one of the clearest cases for URI templates, so let's consider other designs. How does search work on a web site? It works by asking the user to type the search term into a form, and then submitting this form to the server.

				You can do the same with a RESTful design. When a client wishes to search, it'll have to perform an HTTP POST request with a well-formed request body containing the search term(s):

POST /108B618F06644379879B9F2FEA1CAE92 HTTP/1.1
Content-Type: application/json
Accept: application/json

{ "term": "chocolate" }

				The response can either contain the search hits, or a link to a resource containing the hits.

				Both options have potential drawbacks. If you return the search results in the response to the POST request, the client immediately receives the results, but you've lost the ability to cache them.

HTTP/1.1 200 OK
Content-Type: application/json

{
  "term": "chocolate",
  "results": [{
    "name": "Valrhona",
    "link": {
      "href": "http://example.com/B69AD9193F6E472AB75D8EBD97331E22",
      "rel": "product"
  }
  }, {
    "name": "Friis-Holm",
    "link": {
      "href": "http://example.com/3C4BEC72BFB54E1B8FADB16CCB3E7A67",
      "rel": "product"
    }
  }]
}

				On the other hand, if the POST response only replies with a link to the search result, the search results themselves are cacheable, but now the client has to perform an extra request.

HTTP/1.1 303 See Other
Location: http://example.com/0D25692DE79B4AF9A73F128554A9119E

				In both cases, though, you'll need to document the valid representations that a client can POST to the search resource, which means that you need to consider backwards compatibility if you want to change things, and so on. In other words, you can 'protect' the URL schema of the search resource, but not the payload schema.

				If you're already exposing an API over HTTPS, caching is of marginal relevance anyway, so you may decide to use the first POST option outlined above. Sometimes, however, performance is more important than transport security. As an example, I once wrote (for a client) a publicly available music catalogue API. This API isn't protected, because the data isn't secret, and it has to be fast. Resources are cached for 6 hours, so if you're searching for something popular (say, Rihanna), odds are that you're going to get a response from an intermediary cache. URI templates work well in this scenario, but it's a deliberate performance optimisation.

2016-03-10 07:34 UTC

Ruben Verborgh

        One can always avoid hackable URLs with trickery, but would you say that leads to a cleaner solution? I agree that search is "one of the clearest cases for URI templates", so why bother putting in an unneeded POST request just to avoid hackable URIs? I fail to see why this would be a better REST design, given that you loose the beneficial properties of the uniform interface constraints by overloading the POST method for what is essentially a read-only operation. The trade-off here is entirely non-technical: you trade the performance of the interface for the aesthetics of its URLs.

        Here's how a website would do it: it would have an HTML form for a GET request, which would naturally lead to a hackable URL. Because that's how HTML forms work: like URI templates, they result in hackable URLs. It's unavoidable. Hackable URLs are a direct consequence of common hypermedia controls, which are a necessity to realize the hypermedia constraint. You can swim against the current and make more difficult hypermedia controls, but to whose benefit? It's much easier to just accept that hackability is a side-effect of HTML forms and URI templates.

        Therefore, your thesis that one should avoid hackable URIs breaks down at this point. APIs with non-hackable URIs are inherently incompatible with URI templates—and why throw away this excellent means of affordance? After all, the most straightforward way to realize search functionality is with an in-band URI template. And URI templates lead to hackable URIs. Hence, hackable URIs occur in REST APIs. You can work around this, but it's quite artificial to do so. In essence, you're using the POST request to obfuscate the URI; having such a translation step for purely aesthetic motives complicates the API and sacrifices caching for no good reason. Hackable URIs are definitely the lesser evil of the two.

        So, would you avoid hackable URIs at any cost? Or can you agree that they indeed have a place if we like to keep using the hypermedia controls that have been standardized?

2016-03-10 23:50 UTC

Mark Seemann

				Ruben, my intent with this post was to highlight a problem I've experienced when publishing REST APIs, and that I've attempted to explain in the section titled Keep clients safe. I wish that problem didn't exist, but in my experience, it does. It most likely depends on a lot of factors, so if you don't have that problem, you can disregard the entire article.

				The issue isn't that I want to make URLs opaque. The real issue at stake is that I want clients to follow links, because it gives me options to evolve the API. Consider the example in this article. If a client wants to see a user's messages, it should follow links. If, however, a client developer writes the code in such a way that it'll explicitly look for a root > users > user details > user messages route, little is gained.

				A client looking for user messages should be implemented with a prioritised rule set:

Look for a user-messages link. If found, follow it. Done.

Look for a user-details link. If found, follow it, and apply this rule list again from the top.

Look for a user link. If found, follow it, and apply this rule list again from the top.

				Actually, it's more complex than outlined here, because the rule set may have to dynamically change based on the context, but I hope it gets the main point across.

				If client developers can guess, and thereby 'hack', my service's URLs, I can't restructure my API. It's not really a question of aesthetics; it's not that I want to be able to rename URLs. The real issue is that sometimes, when developing software, you discover that you've modelled a concept the wrong way. Perhaps you've conflated two concepts that ought to be treated as separate resources. Perhaps you've create a false distinction, but now you realise that two concepts that you thought were separate, is really the same. Therefore, sometimes you want to split one type of resource into two, or more, or you want to combine two or more different types of resources into one.

				If clients follow links, you have a chance to pull off such radical restructuring. If clients have hard-coded URLs, you're out of luck.

2016-03-12 16:28 UTC

Ashley Aitken

				Thank you for an interesting post and discussion.  I am no expert wrt REST but I have a comment
				(posed as a question) and a question.

				If client developers abuse transparent URLs (that are provided in a level 3 api)
				isn't the fact that the client stops working when the URLs change their problem?
				And sure, they can guess other URLs, just like in the browser we can guess other
				pages. Doesn't mean the application stage should let them access those pages etc.

				And now the question.  Are there any libraries or tools that allow REST client
				applications to easily get a list of the links and "follow the links" (i.e. do
				an API call)?  I can see a client having slots for possible / expected links,
				filling those that exist, and calling them. Is there any library to help this?

2016-06-23 15:18 UTC

Mark Seemann

				Ashley, thank you for writing. In the end, as you point out, it's the client's problem if they don't adhere to the contract. In some scenarios, that's the end of the story. In other scenarios (e.g. the ones I often find myself in), there's a major client that has a lot of clout, and if those developers find that you've moved their cheese, their default reaction is that it's your fault - not theirs. In such cases, it can be political to ensure that there's no doubt about the contract.

				I'm not aware of any libraries apart from HttpClient. It doesn't do any of the things you ask about, because it's too low-level, but at least it enables you to build a sensible client API on top of it.

2016-06-23 21:05 UTC


text:
// FUTURE PLANS
//  extend map type concept to cover custom map layers via OL in exhibit; this could be done with the generic mapConstructor setting, but perhaps a less programming intensive way can be made available
//  change popup on polygon to location of click instead of centroid
//  hide/show instead of erase/redraw features
//  test cases
//  incorporate an editor based on OL editing API

Exhibit.OLMapView = function(containerElmt, uiContext) {
    Exhibit.OLMapView._initialize();

    this._div = containerElmt;
    this._uiContext = uiContext;

    this._settings = {};
    this._accessors = {
        getProxy:    function(itemID, database, visitor) { visitor(itemID); },
        getColorKey: null,
        getSizeKey:  null,
        getIconKey:  null,
        getIcon:     null
    };
    this._colorCoder = null;
    this._sizeCoder = null;
    this._iconCoder = null;

    this._selectListener = null;
    this._itemIDToMarker = {};

    var view = this;
    this._listener = {
        onItemsChanged: function() {
            view._reconstruct();
        }
    };
    uiContext.getCollection().addListener(this._listener);
};

Exhibit.OLMapView.contexts = {};

Exhibit.OLMapView._settingSpecs = {
    "latlngOrder":      { type: "enum",     defaultValue: "latlng", choices: [ "latlng", "lnglat" ] },
    "latlngPairSeparator": { type: "text",  defaultValue: ";"   },
    "center":           { type: "float",    defaultValue: null,     dimensions: 2 },
    "zoom":             { type: "float",    defaultValue: null      },

    "scrollWheelZoom":  { type: "boolean",  defaultValue: true      },
    "scaleControl":     { type: "boolean",  defaultValue: true      },
    "overviewControl":  { type: "boolean",  defaultValue: false     },
    "type":             { type: "enum",     defaultValue: "osm", choices: [ "osm", "wms", "gmap", "gsat", "ghyb", "gter", "vmap", "vsat", "vhyb", "ymap", "ysat", "yhyb" ] },
    "bubbleTip":        { type: "enum",     defaultValue: "top",    choices: [ "top", "bottom" ] },
    "mapHeight":        { type: "int",      defaultValue: 400       },
    "mapConstructor":   { type: "function", defaultValue: null      },
    "projection":       { type: "function", defaultValue: null      },
    "color":            { type: "text",     defaultValue: "#F9B200" }, //"#FF9000" },
    "colorCoder":       { type: "text",     defaultValue: null      },
    "sizeCoder":        { type: "text",     defaultValue: null      },
    "iconCoder":        { type: "text",     defaultValue: null      },
    "selectCoordinator":  { type: "text",   defaultValue: null      },

    "iconSize":         { type: "int",      defaultValue: 0         },
    "iconFit":          { type: "text",     defaultValue: "smaller" },
    "iconScale":        { type: "float",    defaultValue: 1         },
    "iconOffsetX":      { type: "float",    defaultValue: 0         },
    "iconOffsetY":      { type: "float",    defaultValue: 0         },
    "shape":            { type: "text",     defaultValue: "circle"  },
    "shapeWidth":       { type: "int",      defaultValue: 24        },
    "shapeHeight":      { type: "int",      defaultValue: 24        },
    "shapeAlpha":       { type: "float",    defaultValue: 0.7       },
    "pin":              { type: "boolean",  defaultValue: true      },
    "pinHeight":        { type: "int",      defaultValue: 6         },
    "pinWidth":         { type: "int",      defaultValue: 6         },

    "borderOpacity":    { type: "float",    defaultValue: 0.5       },
    "borderWidth":      { type: "int",      defaultValue: 1         },
    "borderColor":      { type: "text",     defaultValue: null      },
    "opacity":          { type: "float",    defaultValue: 1			}, //0.7       },

    "sizeLegendLabel":  { type: "text",     defaultValue: null      },
    "colorLegendLabel": { type: "text",     defaultValue: null      },
    "iconLegendLabel":  { type: "text",     defaultValue: null      },
    "markerScale":      { type: "text",     defaultValue: null      },
    "showHeader":       { type: "boolean",  defaultValue: true      },
    "showSummary":      { type: "boolean",  defaultValue: true      },
    "showFooter":       { type: "boolean",  defaultValue: true      },
    "showToolbox":      { type: "boolean",  defaultValue: true      },
    "osmURL":           { type: "text",     defaultValue: "http://tah.openstreetmap.org/Tiles/tile/${z}/${x}/${y}.png" },
    "wmsURL":           { type: "text",     defaultValue: "http://labs.metacarta.com/wms/vmap0" }
};

Exhibit.OLMapView._accessorSpecs = [
    {   accessorName:   "getProxy",
        attributeName:  "proxy"
    },
    {   accessorName: "getLatlng",
        alternatives: [
            {   bindings: [
                    {   attributeName:  "latlng",
                        types:          [ "float", "float" ],
                        bindingNames:   [ "lat", "lng" ]
                    },
                    {   attributeName:  "maxAutoZoom",
                        type:           "float",
                        bindingName:    "maxAutoZoom",
                        optional:       true
                    }
                ]
            },
            {   bindings: [
                    {   attributeName:  "lat",
                        type:           "float",
                        bindingName:    "lat"
                    },
                    {   attributeName:  "lng",
                        type:           "float",
                        bindingName:    "lng"
                    },
                    {   attributeName:  "maxAutoZoom",
                        type:           "float",
                        bindingName:    "maxAutoZoom",
                        optional:       true
                    }
                ]
            }
        ]
    },
    {   accessorName:   "getPolygon",
        attributeName:  "polygon",
        type:           "text"
    },
    {   accessorName:   "getPolyline",
        attributeName:  "polyline",
        type:           "text"
    },
    {   accessorName:   "getColorKey",
        attributeName:  "marker", // backward compatibility
        type:           "text"
    },
    {   accessorName:   "getColorKey",
        attributeName:  "colorKey",
        type:           "text"
    },
    {   accessorName:   "getSizeKey",
        attributeName:  "sizeKey",
        type:           "text"
    },
    {   accessorName:   "getIconKey",
        attributeName:  "iconKey",
        type:           "text"
    },
    {   accessorName:   "getIcon",
        attributeName:  "icon",
        type:           "url"
    }
];

Exhibit.OLMapView._initialize = function() {
    var links = [];
    var heads = document.documentElement.getElementsByTagName("head");
    for (var h = 0; h < heads.length; h++) {
        var linkElmts = heads[h].getElementsByTagName("link");
        for (var l = 0; l < linkElmts.length; l++) {
            var link = linkElmts[l];
            if (link.rel.match(/\bexhibit\/map-painter\b/)) {
                Exhibit.OLMapView._markerUrlPrefix = link.href + "?";
            }
        }
    }
    Exhibit.OLMapView._initialize = function() {};
};

Exhibit.OLMapView.create = function(configuration, containerElmt, uiContext) {
    var view = new Exhibit.OLMapView(
        containerElmt,
        Exhibit.UIContext.create(configuration, uiContext)
    );
    Exhibit.OLMapView._configure(view, configuration);

    view._internalValidate();
    view._initializeUI();
    return view;
};

Exhibit.OLMapView.createFromDOM = function(configElmt, containerElmt, uiContext) {
    var configuration = Exhibit.getConfigurationFromDOM(configElmt);
    var view = new Exhibit.OLMapView(
        containerElmt != null ? containerElmt : configElmt,
        Exhibit.UIContext.createFromDOM(configElmt, uiContext)
    );

    Exhibit.SettingsUtilities.createAccessorsFromDOM(configElmt, Exhibit.OLMapView._accessorSpecs, view._accessors);
    Exhibit.SettingsUtilities.collectSettingsFromDOM(configElmt, Exhibit.OLMapView._settingSpecs, view._settings);
    Exhibit.OLMapView._configure(view, configuration);

    view._internalValidate();
    view._initializeUI();
    return view;
};

Exhibit.OLMapView._configure = function(view, configuration) {
    Exhibit.SettingsUtilities.createAccessors(configuration, Exhibit.OLMapView._accessorSpecs, view._accessors);
    Exhibit.SettingsUtilities.collectSettings(configuration, Exhibit.OLMapView._settingSpecs, view._settings);

    var accessors = view._accessors;
    view._getLatlng = accessors.getLatlng != null ?
        function(itemID, database, visitor) {
            accessors.getProxy(itemID, database, function(proxy) {
                accessors.getLatlng(proxy, database, visitor);
            });
        } :
        null;
};

Exhibit.OLMapView.prototype.dispose = function() {
    this._uiContext.getCollection().removeListener(this._listener);

    this._map.destroy();
    this._map = null;

    if (this._selectListener != null) {
        this._selectListener.dispose();
        this._selectListener = null;
    }
    this._itemIDToMarker = {};

    if (this._settings.showToolbox) {
        this._toolboxWidget.dispose();
        this._toolboxWidget = null;
    }

    this._dom.dispose();
    this._dom = null;

    this._uiContext.dispose();
    this._uiContext = null;

    this._div.innerHTML = "";
    this._div = null;
};

Exhibit.OLMapView.prototype._internalValidate = function() {
    var exhibit = this._uiContext.getExhibit();
    if (this._accessors.getColorKey != null) {
        if (this._settings.colorCoder != null) {
            this._colorCoder = exhibit.getComponent(this._settings.colorCoder);
        }

        if (this._colorCoder == null) {
            this._colorCoder = new Exhibit.DefaultColorCoder(this._uiContext);
        }
    }
    if (this._accessors.getSizeKey != null) {
        if (this._settings.sizeCoder != null) {
            this._sizeCoder = exhibit.getComponent(this._settings.sizeCoder);
            if ("markerScale" in this._settings) {
                this._sizeCoder._settings.markerScale = this._settings.markerScale;
            }
        }
    }
    if (this._accessors.getIconKey != null) {
        if (this._settings.iconCoder != null) {
            this._iconCoder = exhibit.getComponent(this._settings.iconCoder);
        }
    }
    if ("selectCoordinator" in this._settings) {
        var selectCoordinator = exhibit.getComponent(this._settings.selectCoordinator);
        if (selectCoordinator != null) {
            var self = this;
            this._selectListener = selectCoordinator.addListener(function(o) {
                self._select(o);
            });
        }
    }
};

Exhibit.OLMapView.prototype._initializeUI = function() {
    var self = this;

    var legendWidgetSettings = {};
    legendWidgetSettings.colorGradient = (this._colorCoder != null && "_gradientPoints" in this._colorCoder);
    legendWidgetSettings.colorMarkerGenerator = this._createColorMarkerGenerator();
    legendWidgetSettings.sizeMarkerGenerator = this._createSizeMarkerGenerator();
    legendWidgetSettings.iconMarkerGenerator = this._createIconMarkerGenerator();

    this._div.innerHTML = "";
    this._dom = Exhibit.ViewUtilities.constructPlottingViewDom(
        this._div,
        this._uiContext,
        this._settings.showSummary && this._settings.showHeader,
        {   onResize: function() {
                self._map.checkResize();
            }
        },
        legendWidgetSettings
    );

    if (this._settings.showToolbox) {
        this._toolboxWidget = Exhibit.ToolboxWidget.createFromDOM(this._div, this._div, this._uiContext);
    }

    var mapDiv = this._dom.plotContainer;
    mapDiv.style.height = this._settings.mapHeight + "px";
    mapDiv.className = "exhibit-mapView-map";

    this._map = this._constructMap(mapDiv);
    this._reconstruct();
};

Exhibit.OLMapView.prototype._constructMap = function(mapDiv) {
    var settings = this._settings;

    if (settings.projection != null) {
	this._projection = settings.projection();
    } else {
        this._projection = new OpenLayers.Projection("EPSG:4326");
    }

    if (settings.mapConstructor != null) {
    	console.log("in olmap-view.js get constructor");

        return settings.mapConstructor(mapDiv);
    } else {
        var map = new OpenLayers.Map({
		"div": mapDiv,
		"controls": [],
		"projection": new OpenLayers.Projection("EPSG:4326"),
		//"projection": new OpenLayers.Projection("EPSG:900913"),
		"displayProjection": this._projection,
	        "units": "m",
	        "numZoomLevels": 18,
	        //"maxResolution": 156543.0339,
	        //"maxExtent": new OpenLayers.Bounds(-20037508.34, -20037508.34, 20037508.34, 20037508.34)
	        "maxExtent": new OpenLayers.Bounds(-181, -91, 181, 91)
        });

        var osm = new OpenLayers.Layer.OSM(
                      "StreetAAAA",
                      settings.osmURL,
                      { "wrapDateLine": true });
	osm.setVisibility(false);
        var wms = new OpenLayers.Layer.WMS(
		       "World Map",
		       settings.wmsURL,
                       {"layers": "basic" }, {"wrapDateLine": true});
	wms.setVisibility(false);

	//neu CH
	var availableLayers = [wms];
	var availability = { "wms" : wms };
	//end neu CH

	//var availableLayers = [osm, wms];
	//var availability = { "osm" : osm, "wms" : wms };

	if (typeof G_HYBRID_MAP != "undefined") {
	    var gmap = new OpenLayers.Layer.Google(
		       "Street (Google)",
                       { "sphericalMercator": true });
            gmap.setVisibility(false);
	    var gsat = new OpenLayers.Layer.Google(
		       "Satellite (Google)",
                       { "type": G_SATELLITE_MAP, "sphericalMercator": true });
	    gsat.setVisibility(false);
	    var ghyb = new OpenLayers.Layer.Google(
		       "Street (Google)",
                       { "type": G_HYBRID_MAP, "sphericalMercator": true });
	    ghyb.setVisibility(false);
	    var gter = new OpenLayers.Layer.Google(
		       "Terrain (Google)",
                       { "type": G_PHYSICAL_MAP, "sphericalMercator": true });
	    gter.setVisibility(false);
	    availableLayers.push(gmap, gsat, ghyb, gter);
	    availability["gmap"] = gmap;
	    availability["gsat"] = gsat;
	    availability["ghyb"] = ghyb;
	    availability["gter"] = gter;
	}

	if (typeof VEMapStyle != "undefined") {
            var vmap = new OpenLayers.Layer.VirtualEarth(
                       "Street (Virtual Earth)",
                       {"type": VEMapStyle.Road, "sphericalMercator": true});
	    vmap.setVisibility(false);
	    var vsat = new OpenLayers.Layer.VirtualEarth(
                       "Satellite (Virtual Earth)",
                       {"type": VEMapStyle.Aerial, "sphericalMercator": true});
	    vsat.setVisibility(false);
	    var vhyb = new OpenLayers.Layer.VirtualEarth(
                       "Street (Virtual Earth)",
                       {"type": VEMapStyle.Hybrid, "sphericalMercator": true});
	    vhyb.setVisibility(false);
	    availableLayers.push(vmap, vsat, vhyb);
	    availability["vmap"] = vmap;
	    availability["vsat"] = vsat;
	    availability["vhyb"] = vhyb;
	}

	if (typeof YAHOO_MAP_SAT != "undefined") {
	    var ymap = new OpenLayers.Layer.Yahoo(
		       "Street (Yahoo)",
                       {"sphericalMercator": true});
	    ymap.setVisibility(false);
	    var ysat = new OpenLayers.Layer.Yahoo(
		       "Satellite (Yahoo)",
                       {"type": YAHOO_MAP_SAT, "sphericalMercator": true});
	    ysat.setVisibility(false);
	    var yhyb = new OpenLayers.Layer.Yahoo(
                       "Yahoo Hybrid",
                       {"type": YAHOO_MAP_HYB, "sphericalMercator": true});
	    yhyb.setVisibility(false);
	    availableLayers.push(ymap, ysat, yhyb);
	    availability["ymap"] = ymap;
	    availability["ysat"] = ysat;
	    availability["yhyb"] = yhyb;
	}

	var vectors = new OpenLayers.Layer.Vector("Features", { "wrapDateLine": true });
	availableLayers.push(vectors);

	if (typeof availability[settings.type] != "undefined") {
	    availability[settings.type].setVisibility(true);
	} else {
	    osm.setVisibility(true);
	}

        map.addLayers(availableLayers);
	availability = null;
	availableLayers = null;

        if (settings.center != null && typeof settings.center[0] != "undefined" && typeof settings.center[1] != "undefined") {
            if (settings.zoom != null) {
                map.setCenter(new OpenLayers.LonLat(settings.center[1], settings.center[0]).transform(this._projection, map.getProjectionObject()), settings.zoom);
            } else {
                map.setCenter(new OpenLayers.LonLat(settings.center[1], settings.center[0]).transform(this._projection, map.getProjectionObject()));
            }
        }

        map.addControl(new OpenLayers.Control.PanPanel());
        if (settings.overviewControl) {
            map.addControl(new OpenLayers.Control.OverviewMap());
        }
        if (settings.scaleControl) {
            map.addControl(new OpenLayers.Control.ZoomPanel());
        }

	var self = this;
        var selectControl = new OpenLayers.Control.SelectFeature(vectors, {
            onSelect: function(feature) {
		self._onFeatureSelect(self, feature);
	    },
	    onUnselect: function(feature) {
	        self._onFeatureUnselect(self, feature);
	    }
	});
        map.addControl(selectControl);
	selectControl.activate();

	map.addControl(new OpenLayers.Control.Navigation(settings.scrollWheelZoom, true, OpenLayers.Handler.MOD_SHIFT, true));

        map.addControl(new OpenLayers.Control.LayerSwitcher());

        map.events.register("click", null, SimileAjax.WindowManager.cancelPopups);
        return map;
    }
};

Exhibit.OLMapView.prototype._createColorMarkerGenerator = function() {
    var shape = this._settings.shape;

    return function(color) {
        return SimileAjax.Graphics.createTranslucentImage(
            Exhibit.OLMapView._markerUrlPrefix +
                "?renderer=map-marker&shape=" + shape +
                "&width=20&height=20&pinHeight=5&background=" + color.substr(1),
            "middle"
        );
    };
};

Exhibit.OLMapView.prototype._createSizeMarkerGenerator = function() {
    var shape = this._settings.shape;

    return function(iconSize) {
        return SimileAjax.Graphics.createTranslucentImage(
            Exhibit.OLMapView._markerUrlPrefix +
                "?renderer=map-marker&shape=" + shape +
                "&width=" + iconSize +
                "&height=" + iconSize +
                "&pinHeight=0",
            "middle"
        );
    };
};

Exhibit.OLMapView.prototype._createIconMarkerGenerator = function() {
    return function(iconURL) {
        elmt = document.createElement('img');
        elmt.src = iconURL;
        elmt.style.verticalAlign = "middle";
        elmt.style.height = "40px";
        return elmt;
    };
};

Exhibit.OLMapView.prototype._clearOverlays = function() {
    var vectorLayer = this._map.getLayersByClass("OpenLayers.Layer.Vector");
    if (vectorLayer.length == 1) {
        vectorLayer[0].destroyFeatures();
    }
    while (this._map.popups.length > 0) {
        this._map.removePopup(this._map.popups[0]);
    }
};

Exhibit.OLMapView.prototype._reconstruct = function() {
    this._clearOverlays();
    this._dom.legendWidget.clear();
    this._itemIDToMarker = {};

    var currentSize = this._uiContext.getCollection().countRestrictedItems();
    var unplottableItems = [];

    if (currentSize > 0) {
        this._rePlotItems(unplottableItems);
    }
    this._dom.setUnplottableMessage(currentSize, unplottableItems);
};

Exhibit.OLMapView.prototype._rePlotItems = function(unplottableItems) {
    var self = this;
    var collection = this._uiContext.getCollection();
    var database = this._uiContext.getDatabase();
    var settings = this._settings;
    var accessors = this._accessors;

    var currentSet = collection.getRestrictedItems();
    var locationToData = {};
    var hasColorKey = (accessors.getColorKey != null);
    var hasSizeKey = (accessors.getSizeKey != null);
    var hasIconKey = (accessors.getIconKey != null);
    var hasIcon = (accessors.getIcon != null);

    var hasPoints = (this._getLatlng != null);
    var hasPolygons = (accessors.getPolygon != null);
    var hasPolylines = (accessors.getPolyline != null);

    var colorCodingFlags = { mixed: false, missing: false, others: false, keys: new Exhibit.Set() };
    var sizeCodingFlags = { mixed: false, missing: false, others: false, keys: new Exhibit.Set() };
    var iconCodingFlags = { mixed: false, missing: false, others: false, keys: new Exhibit.Set() };

    var makeLatLng = settings.latlngOrder == "latlng" ?
    function(first, second) { return new OpenLayers.Geometry.Point(second, first); } :
    function(first, second) { return new OpenLayers.Geometry.Point(first, second); };
    currentSet.visit(function(itemID) {
        var latlngs = [];
        var polygons = [];
        var polylines = [];

        if (hasPoints) {
            self._getLatlng(itemID, database, function(v) { if (v != null && "lat" in v && "lng" in v) latlngs.push(v); });
        }
        if (hasPolygons) {
            accessors.getPolygon(itemID, database, function(v) { if (v != null) polygons.push(v); });
        }
        if (hasPolylines) {
            accessors.getPolyline(itemID, database, function(v) { if (v != null) polylines.push(v); });
        }

        if (latlngs.length > 0 || polygons.length > 0 || polylines.length > 0) {
            var color = self._settings.color;

            var colorKeys = null;
            if (hasColorKey) {
                colorKeys = new Exhibit.Set();
                accessors.getColorKey(itemID, database, function(v) { colorKeys.add(v); });

                color = self._colorCoder.translateSet(colorKeys, colorCodingFlags);
            }

            if (latlngs.length > 0) {
                var sizeKeys = null;
                if (hasSizeKey) {
                    sizeKeys = new Exhibit.Set();
                    accessors.getSizeKey(itemID, database, function(v) { sizeKeys.add(v); });
                }
                var iconKeys = null;
                if (hasIconKey) {
                    iconKeys = new Exhibit.Set();
                    accessors.getIconKey(itemID, database, function(v) { iconKeys.add(v); });
                }
                for (var n = 0; n < latlngs.length; n++) {
                    var latlng = latlngs[n];
                    var latlngKey = latlng.lat + "," + latlng.lng;
                    if (latlngKey in locationToData) {
                        var locationData = locationToData[latlngKey];
                        locationData.items.push(itemID);
                        if (hasColorKey) { locationData.colorKeys.addSet(colorKeys); }
                        if (hasSizeKey) { locationData.sizeKeys.addSet(sizeKeys); }
                        if (hasIconKey) { locationData.iconKeys.addSet(iconKeys); }
                    } else {
                        var locationData = {
                            latlng:     latlng,
                            items:      [ itemID ]
                        };
                        if (hasColorKey) { locationData.colorKeys = colorKeys;}
                        if (hasSizeKey) { locationData.sizeKeys = sizeKeys; }
                        if (hasIconKey) { locationData.iconKeys = iconKeys; }
                        locationToData[latlngKey] = locationData;
                    }
                }
            }

            for (var p = 0; p < polygons.length; p++) {
                self._plotPolygon(itemID, polygons[p], color, makeLatLng);
            }
            for (var p = 0; p < polylines.length; p++) {
                self._plotPolyline(itemID, polylines[p], color, makeLatLng);
            }
        } else {
            unplottableItems.push(itemID);
        }
    });

    var bounds, maxAutoZoom = Infinity;
    var addMarkerAtLocation = function(locationData) {
        var itemCount = locationData.items.length;
        if (!bounds) {
            bounds = new OpenLayers.Bounds();
        }

        var shape = self._settings.shape;

        var color = self._settings.color;
        if (hasColorKey) {
            color = self._colorCoder.translateSet(locationData.colorKeys, colorCodingFlags);
        }
        var iconSize = self._settings.iconSize;
        if (hasSizeKey) {
            iconSize = self._sizeCoder.translateSet(locationData.sizeKeys, sizeCodingFlags);
        }

        var icon = null;
        if (itemCount == 1) {
            if (hasIcon) {
                accessors.getIcon(locationData.items[0], database, function(v) { icon = v; });
            }
        }
        if (hasIconKey) {
            icon = self._iconCoder.translateSet(locationData.iconKeys, iconCodingFlags);
        }

        var icon = Exhibit.OLMapView._makeIcon(
            shape,
            color,
            iconSize,
            itemCount == 1 ? "" : itemCount.toString(),
            icon,
            self._settings
        );
        var point = new OpenLayers.Geometry.Point(locationData.latlng.lng, locationData.latlng.lat).transform(self._projection, self._map.getProjectionObject());
	var layer = self._map.getLayersByClass("OpenLayers.Layer.Vector")[0];
	var marker = new OpenLayers.Feature.Vector(point, {locationData: locationData}, {
		"externalGraphic": icon.url,
		"graphicWidth": icon.size.w,
		"graphicHeight": icon.size.h,
		"graphicXOffset": icon.offset.x,
		"graphicYOffset": icon.offset.y,
		"fillColor": "white",
		"fillOpacity": 1.0,
		"strokeWidth": 0});
        var popup = new OpenLayers.Popup.FramedCloud(
                "markerPoup"+Math.floor(Math.random() * 10000),
                new OpenLayers.LonLat(locationData.latlng.lng, locationData.latlng.lat).transform(self._projection, self._map.getProjectionObject()),
		null,
                self._createInfoWindow(locationData.items).innerHTML,
                icon,
                true,
                function() {
                    SimileAjax.WindowManager.cancelPopups();
                    self._map.removePopup(this);
                });
	marker.popup = popup;
	popup.feature = marker;
	layer.addFeatures([marker]);

        if (maxAutoZoom > locationData.latlng.maxAutoZoom) {
            maxAutoZoom = locationData.latlng.maxAutoZoom;
        }
        bounds.extend(point);

        for (var x = 0; x < locationData.items.length; x++) {
            self._itemIDToMarker[locationData.items[x]] = marker;
        }
    }
    for (var latlngKey in locationToData) {
        addMarkerAtLocation(locationToData[latlngKey]);
    }
    if (hasColorKey) {
        var legendWidget = this._dom.legendWidget;
        var colorCoder = this._colorCoder;
        var keys = colorCodingFlags.keys.toArray().sort();
        if (settings.colorLegendLabel !== null) {
            legendWidget.addLegendLabel(settings.colorLegendLabel, 'color');
        }
        if (colorCoder._gradientPoints != null) {
            var legendGradientWidget = this._dom.legendWidget;
            legendGradientWidget.addGradient(this._colorCoder._gradientPoints);
        } else {
            for (var k = 0; k < keys.length; k++) {
                var key = keys[k];
                var color = colorCoder.translate(key);
                legendWidget.addEntry(color, key);
            }
        }

        if (colorCodingFlags.others) {
            legendWidget.addEntry(colorCoder.getOthersColor(), colorCoder.getOthersLabel());
        }
        if (colorCodingFlags.mixed) {
            legendWidget.addEntry(colorCoder.getMixedColor(), colorCoder.getMixedLabel());
        }
        if (colorCodingFlags.missing) {
            legendWidget.addEntry(colorCoder.getMissingColor(), colorCoder.getMissingLabel());
        }
    }

    if (hasSizeKey) {
        var legendWidget = this._dom.legendWidget;
        var sizeCoder = this._sizeCoder;
        var keys = sizeCodingFlags.keys.toArray().sort();
        if (settings.sizeLegendLabel !== null) {
            legendWidget.addLegendLabel(settings.sizeLegendLabel, 'size');
        }
        if (sizeCoder._gradientPoints != null) {
            var points = sizeCoder._gradientPoints;
            var space = (points[points.length - 1].value - points[0].value)/5;
            keys = [];
            for (var i = 0; i < 6; i++) { keys.push(Math.floor(points[0].value + space*i)); }
            for (var k = 0; k < keys.length; k++) {
                var key = keys[k];
                var size = sizeCoder.translate(key);
                legendWidget.addEntry(size, key, 'size');
            }
           } else {
            for (var k = 0; k < keys.length; k++) {
                var key = keys[k];
                var size = sizeCoder.translate(key);
                legendWidget.addEntry(size, key, 'size');
            }
            if (sizeCodingFlags.others) {
                legendWidget.addEntry(sizeCoder.getOthersSize(), sizeCoder.getOthersLabel(), 'size');
            }
            if (sizeCodingFlags.mixed) {
                legendWidget.addEntry(sizeCoder.getMixedSize(), sizeCoder.getMixedLabel(), 'size');
            }
            if (sizeCodingFlags.missing) {
                legendWidget.addEntry(sizeCoder.getMissingSize(), sizeCoder.getMissingLabel(), 'size');
            }
        }
    }

    if (hasIconKey) {
        var legendWidget = this._dom.legendWidget;
        var iconCoder = this._iconCoder;
        var keys = iconCodingFlags.keys.toArray().sort();
        if (settings.iconLegendLabel != null) {
            legendWidget.addLegendLabel(settings.iconLegendLabel, 'icon');
        }
        for (var k = 0; k < keys.length; k++) {
            var key = keys[k];
            var icon = iconCoder.translate(key);
            legendWidget.addEntry(icon, key, 'icon');
        }
        if (iconCodingFlags.others) {
            legendWidget.addEntry(iconCoder.getOthersIcon(), iconCoder.getOthersLabel(), 'icon');
        }
        if (iconCodingFlags.mixed) {
            legendWidget.addEntry(iconCoder.getMixedIcon(), iconCoder.getMixedLabel(), 'icon');
        }
        if (iconCodingFlags.missing) {
            legendWidget.addEntry(iconCoder.getMissingIcon(), iconCoder.getMissingLabel(), 'icon');
        }
    }

    if (bounds && settings.zoom == null) {
        if (maxAutoZoom > 12) maxAutoZoom = 12;
        var zoom = Math.max(0, self._map.getZoomForExtent(bounds) - 1);
        zoom = Math.min(zoom, maxAutoZoom);
        self._map.zoomTo(zoom);
        console.log("zoom null");
    } else {
    	//CH NEW
    	settings.zoom = 0;
    	//CH END
    	self._map.zoomTo(settings.zoom);
    }

    if (bounds && settings.center == null) {
        self._map.setCenter(bounds.getCenterLonLat());
    }
};

Exhibit.OLMapView.prototype._plotPolygon = function(itemID, polygonString, color, makeLatLng) {
    var coords = this._parsePolygonOrPolyline(polygonString, makeLatLng);
    if (coords.length > 1) {
        var settings = this._settings;
        var borderColor = settings.borderColor != null ? settings.borderColor : color;
        var polygon = new OpenLayers.Geometry.LinearRing(coords).transform(this._projection, this._map.getProjectionObject());
	var polygonStyle = {
	    "strokeColor": borderColor,
	    "strokeWidth": settings.borderWidth,
	    "strokeOpacity": settings.borderOpacity,
	    "fillColor": "none",
	    "fillOpacity": settings.opacity,
	    "strokeDashstyle":"solid",
	    "pointRadius":1
	};
	var polygonFeature = new OpenLayers.Feature.Vector(polygon, null, polygonStyle);

        return this._addPolygonOrPolyline(itemID, polygonFeature);
    }
    return null;
};

Exhibit.OLMapView.prototype._plotPolyline = function(itemID, polylineString, color, makeLatLng) {
    var coords = this._parsePolygonOrPolyline(polylineString, makeLatLng);
    if (coords.length > 1) {
        var settings = this._settings;
        var borderColor = settings.borderColor != null ? settings.borderColor : color;
        var polyline = new OpenLayers.Geometry.LineString(coords).transform(this._projection, this._map.getProjectionObject());
	var polylineStyle = {
	    "strokeColor": borderColor,
	    "strokeWidth": settings.borderWidth,
	    "strokeOpacity": settings.borderOpacity
	};
	var polylineFeature = new OpenLayers.Feature.Vector(polyline, null, polylineStyle);

        return this._addPolygonOrPolyline(itemID, polylineFeature);
    }
    return null;
};

Exhibit.OLMapView.prototype._addPolygonOrPolyline = function(itemID, poly) {
    var vectors = this._map.getLayersByClass("OpenLayers.Layer.Vector");
    if (vectors.length > 0) {
	var vectorLayer = vectors[0];
	vectorLayer.addFeatures([poly]);
    } else {
	return null;
    }

    var self = this;
    var centroid = poly.geometry.getCentroid();

    //CH
    if (centroid != null) {
    //CH end
    var popup = new OpenLayers.Popup.FramedCloud(
        "vectorPopup"+Math.floor(Math.random() * 10000),
        new OpenLayers.LonLat(centroid.x, centroid.y),
        null,
        self._createInfoWindow([ itemID ]).innerHTML,
        null,
        true,
        function() {
            SimileAjax.WindowManager.cancelPopups();
            self._map.removePopup(this);
        });
    poly.popup = popup;
    popup.feature = poly;

    this._itemIDToMarker[itemID] = poly;

    return poly;
    //CH
    } else return null;
    //CH end
};

Exhibit.OLMapView.prototype._parsePolygonOrPolyline = function(s, makeLatLng) {
    var coords = [];

    var a = s.split(this._settings.latlngPairSeparator);
    for (var i = 0; i < a.length; i++) {
        var pair = a[i].split(",");
        coords.push(makeLatLng(parseFloat(pair[0]), parseFloat(pair[1])));
    }

    return coords;
};

Exhibit.OLMapView.prototype._onFeatureSelect = function(self, feature) {
    self._map.addPopup(feature.popup, true);
    if (self._selectListener != null) {
        self._selectListener.fire({ itemIDs: feature.attributes.locationData.items });
    }
};

Exhibit.OLMapView.prototype._onFeatureUnselect = function(self, feature) {
    SimileAjax.WindowManager.cancelPopups();
    self._map.removePopup(feature.popup);
};

Exhibit.OLMapView.prototype._select = function(selection) {
    var self = this;
    var itemID = selection.itemIDs[0];
    var marker = this._itemIDToMarker[itemID];
    if (marker) {
        self._map.addPopup(marker.popup, true);
    }
};

Exhibit.OLMapView.prototype._createInfoWindow = function(items) {
    var contextId = "context"+Math.random()*1000;
    var selfuic = this._uiContext;
    var selfdb = this._uiContext.getDatabase();
    var selfreg = this._uiContext.getLensRegistry();
    var olContext = {};
    olContext.getSetting = function(setting) {
	return selfuic.getSetting(setting);
    };
    olContext.getDatabase = function() {
	return selfdb;
    };
    olContext.getLensRegistry = function() {
	return selfreg;
    };
    olContext.isBeingEdited = function(a) {
	return false;
    };
    olContext.formatList = function(iterator, count, valueType, appender) {
	return selfuic.formatList(iterator, count, valueType, appender);
    };
    olContext.format = function(value, valueType, appender) {
        // assume this is the only thing being done with context,
	// and that the only thing be formmatted is items; currently
	// a safe assumption
	var f = new Exhibit.Formatter._ItemFormatter(olContext);
        f.format = function(v, a) {
	    var title = this.formatText(v);
	    Exhibit.OLMapView.contexts[contextId] = selfuic;
	    // it seems OpenLayers quashes events that are
	    // programatically registered?  sneaking it in as an
	    // attribute is the only thing that works, sadly.
	    // the contexts array is a hack to coordinate global
	    // variables with a randomly ID'd view context.
	    var anchor = SimileAjax.DOM.createElementFromString("<a href=\"" + Exhibit.Persistence.getItemLink(v) + "\" class='exhibit-item' onclick='Exhibit.UI.showItemInPopup(\""+v+"\", this, Exhibit.OLMapView.contexts[\""+contextId+"\"]); return false;'>" + title + "</a>");
	    a(anchor);
	};
        f.format(value, appender);
    };
    return Exhibit.ViewUtilities.fillBubbleWithItems(
        null,
        items,
        olContext
    );
};

Exhibit.OLMapView._iconData = null;
Exhibit.OLMapView._markerUrlPrefix = "http://service.simile-widgets.org/painter/painter?";
Exhibit.OLMapView._defaultMarkerShape = "circle";

Exhibit.OLMapView._makeIcon = function(shape, color, iconSize, label, iconURL, settings) {
    var extra = label.length * 3;
    var halfWidth = Math.ceil(settings.shapeWidth / 2) + extra;
    var bodyHeight = settings.shapeHeight;
    var width = halfWidth * 2;
    var height = bodyHeight;
    if (iconSize > 0) {
        width = iconSize;
        halfWidth = Math.ceil(iconSize / 2);
        height = iconSize;
        bodyHeight = iconSize;
        settings.pin = false;
    }

    var icon = new Object();
    var imageParameters = [
        "renderer=map-marker",
        "shape=" + shape,
        "alpha=" + settings.shapeAlpha,
        "width=" + width,
        "height=" + bodyHeight,
        "background=" + color.substr(1),
        "label=" + label
    ];
    // no shadows for OL yet
    //var shadowParameters = [
    //    "renderer=map-marker-shadow",
    //    "shape=" + shape,
    //    "width=" + width,
    //    "height=" + bodyHeight
    //];
    var pinParameters = [];

    if (iconURL != null) {
        imageParameters.push("icon=" + iconURL);
        if (settings.iconFit != "smaller") {
            imageParameters.push("iconFit=" + settings.iconFit);
        }
        if (settings.iconScale != 1) {
            imageParameters.push("iconScale=" + settings.iconScale);
        }
        if (settings.iconOffsetX != 1) {
            imageParameters.push("iconX=" + settings.iconOffsetX);
        }
        if (settings.iconOffsetY != 1) {
            imageParameters.push("iconY=" + settings.iconOffsetY);
        }
    }

    if (settings.pin) {
        var pinHeight = settings.pinHeight;
        var pinHalfWidth = Math.ceil(settings.pinWidth / 2);

        height += pinHeight;

        pinParameters.push("pinHeight=" + pinHeight);
        pinParameters.push("pinWidth=" + (pinHalfWidth * 2));

        icon.iconAnchor = new OpenLayers.Pixel(-halfWidth, -height);
        icon.imageMap = [
            0, 0,
            0, bodyHeight,
            halfWidth - pinHalfWidth, bodyHeight,
            halfWidth, height,
            halfWidth + pinHalfWidth, bodyHeight,
            width, bodyHeight,
            width, 0
        ];
        icon.shadowSize = new OpenLayers.Size(width * 1.5, height - 2);
        icon.infoWindowAnchor = (settings.bubbleTip == "bottom") ? new OpenLayers.Pixel(halfWidth, height) : new OpenLayers.Pixel(halfWidth, 0);
    } else {
        pinParameters.push("pin=false");

        icon.iconAnchor = new OpenLayers.Pixel(-halfWidth, -Math.ceil(height / 2));
        icon.imageMap = [
            0, 0,
            0, bodyHeight,
            width, bodyHeight,
            width, 0
        ];
        icon.infoWindowAnchor = new OpenLayers.Pixel(halfWidth, 0);
    }

    icon.image = Exhibit.OLMapView._markerUrlPrefix + imageParameters.concat(pinParameters).join("&") + "&.png";
    // if (iconSize == 0) { icon.shadow = Exhibit.OLMapView._markerUrlPrefix + shadowParameters.concat(pinParameters).join("&") + "&.png"; }
    icon.iconSize = new OpenLayers.Size(width, height);
    // icon.shadowSize = new OpenLayers.Size(width * 1.5, height - 2);

    var olicon = new OpenLayers.Icon(icon.image, icon.iconSize, icon.iconAnchor);

    return olicon;
};

redpajama_set_name: RedPajamaGithub


text:
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
   File Name:          statement-008.js
   Corresponds To:     12.8-1-n.js
   ECMA Section:       12.8 The break statement
   Description:

   Author:             christine@netscape.com
   Date:               12 november 1997
*/
var SECTION = "statement-008";
var VERSION = "JS1_4";
var TITLE   = "The break in statement";

startTest();
writeHeaderToLog( SECTION + " "+ TITLE);

var result = "Failed";
var exception = "No exception thrown";
var expect = "Passed";

try {
  eval("break;");
} catch ( e ) {
  result = expect;
  exception = e.toString();
}

new TestCase(
  SECTION,
  "break outside of an iteration statement" +
  " (threw " + exception +")",
  expect,
  result );

test();


text:
/*global $STM_Config */
import koa_router from 'koa-router';
import koa_body from 'koa-body';
import config from 'config';
import { getRemoteIp, rateLimitReq, checkCSRF } from 'server/utils/misc';
import coBody from 'co-body';
import Mixpanel from 'mixpanel';
import { PublicKey, Signature, hash } from '@steemit/steem-js/lib/auth/ecc';
import { api } from '@steemit/steem-js';
import fetch from 'node-fetch';

const ACCEPTED_TOS_TAG = 'accepted_tos_20180614';

const walletApiURI = $STM_Config.wallet_url + '/api/v1/tron';

const mixpanel = config.get('mixpanel')
    ? Mixpanel.init(config.get('mixpanel'))
    : null;

const _stringval = v => (typeof v === 'string' ? v : JSON.stringify(v));

const _parse = params => {
    if (typeof params === 'string') {
        try {
            return JSON.parse(params);
        } catch (error) {
            console.error('json_parse', error, params);
            return {};
        }
    } else {
        return params;
    }
};

function logRequest(path, ctx, extra) {
    let d = { ip: getRemoteIp(ctx.req) };
    if (ctx.session) {
        if (ctx.session.user) {
            d.user = ctx.session.user;
        }
        if (ctx.session.uid) {
            d.uid = ctx.session.uid;
        }
        if (ctx.session.a) {
            d.account = ctx.session.a;
        }
    }
    if (extra) {
        Object.keys(extra).forEach(k => {
            const nk = d[k] ? '_' + k : k;
            d[nk] = extra[k];
        });
    }
    const info = Object.keys(d)
        .map(k => `${k}=${_stringval(d[k])}`)
        .join(' ');
    console.log(`-- /${path} --> ${info}`);
}

export default function useGeneralApi(app) {
    const router = koa_router({ prefix: '/api/v1' });
    app.use(router.routes());
    const koaBody = koa_body();

    router.post('/login_account', koaBody, function*() {
        // if (rateLimitReq(this, this.req)) return;
        const params = this.request.body;
        const { csrf, account, signatures } = _parse(params);
        if (!checkCSRF(this, csrf)) return;

        logRequest('login_account', this, { account });
        try {
            if (signatures) {
                if (!this.session.login_challenge) {
                    console.error(
                        '/login_account missing this.session.login_challenge'
                    );
                } else {
                    const [chainAccount] = yield api.getAccountsAsync([
                        account,
                    ]);
                    if (!chainAccount) {
                        console.error(
                            '/login_account missing blockchain account',
                            account
                        );
                    } else {
                        const auth = { posting: false };
                        const bufSha = hash.sha256(
                            JSON.stringify(
                                { token: this.session.login_challenge },
                                null,
                                0
                            )
                        );
                        const verify = (
                            type,
                            sigHex,
                            pubkey,
                            weight,
                            weight_threshold
                        ) => {
                            if (!sigHex) return;
                            if (weight !== 1 || weight_threshold !== 1) {
                                console.error(
                                    `/login_account login_challenge unsupported ${
                                        type
                                    } auth configuration: ${account}`
                                );
                            } else {
                                const sig = parseSig(sigHex);
                                const public_key = PublicKey.fromString(pubkey);
                                const verified = sig.verifyHash(
                                    bufSha,
                                    public_key
                                );
                                if (!verified) {
                                    console.error(
                                        '/login_account verification failed',
                                        this.session.uid,
                                        account,
                                        pubkey
                                    );
                                }
                                auth[type] = verified;
                            }
                        };
                        const {
                            posting: {
                                key_auths: [[posting_pubkey, weight]],
                                weight_threshold,
                            },
                        } = chainAccount;
                        verify(
                            'posting',
                            signatures.posting,
                            posting_pubkey,
                            weight,
                            weight_threshold
                        );
                        if (auth.posting) this.session.a = account;
                    }
                }
            }
            // login checkpoint
            api.call(
                'overseer.collect',
                [
                    'custom',
                    {
                        measurement: 'user_login',
                        tags: {
                            entry: 'condenser',
                        },
                        fields: {
                            username: account,
                        },
                    },
                ],
                error => {
                    if (error) console.warn('overseer error', error);
                }
            );

            this.body = JSON.stringify({
                status: 'ok',
            });
            const remote_ip = getRemoteIp(this.req);
            if (mixpanel) {
                mixpanel.people.set(this.session.uid, {
                    ip: remote_ip,
                    $ip: remote_ip,
                });
                mixpanel.people.increment(this.session.uid, 'Logins', 1);
            }
        } catch (error) {
            console.error(
                'Error in /login_account api call',
                this.session.uid,
                error.message
            );
            this.body = JSON.stringify({
                error: error.message,
            });
            this.status = 500;
        }
    });

    router.post('/logout_account', koaBody, function*() {
        // if (rateLimitReq(this, this.req)) return; - logout maybe immediately followed with login_attempt event
        const params = this.request.body;
        const { csrf } = _parse(params);
        if (!checkCSRF(this, csrf)) return;
        logRequest('logout_account', this);
        try {
            this.session.a = null;
            this.body = JSON.stringify({ status: 'ok' });
        } catch (error) {
            console.error(
                'Error in /logout_account api call',
                this.session.uid,
                error
            );
            this.body = JSON.stringify({ error: error.message });
            this.status = 500;
        }
    });

    router.post('/csp_violation', function*() {
        if (rateLimitReq(this, this.req)) return;
        let params;
        try {
            params = yield coBody(this);
        } catch (error) {
            console.log('-- /csp_violation error -->', error);
        }
        if (params && params['csp-report']) {
            const csp_report = params['csp-report'];
            const value = `${csp_report['document-uri']} : ${
                csp_report['blocked-uri']
            }`;
            console.log(
                '-- /csp_violation -->',
                value,
                '--',
                this.req.headers['user-agent']
            );
        } else {
            console.log(
                '-- /csp_violation [no csp-report] -->',
                params,
                '--',
                this.req.headers['user-agent']
            );
        }
        this.body = '';
    });

    router.post('/setUserPreferences', koaBody, function*() {
        const params = this.request.body;
        const { csrf, payload } = _parse(params);
        if (!checkCSRF(this, csrf)) return;
        console.log(
            '-- /setUserPreferences -->',
            this.session.user,
            this.session.uid,
            payload
        );
        if (!this.session.a) {
            this.body = 'missing logged in account';
            this.status = 500;
            return;
        }
        try {
            const json = JSON.stringify(payload);
            if (json.length > 1024) throw new Error('the data is too long');
            this.session.user_prefs = json;
            this.body = JSON.stringify({ status: 'ok' });
        } catch (error) {
            console.error(
                'Error in /setUserPreferences api call',
                this.session.uid,
                error
            );
            this.body = JSON.stringify({ error: error.message });
            this.status = 500;
        }
    });

    router.post('/isTosAccepted', koaBody, function*() {
        const params = this.request.body;
        const { csrf } = _parse(params);
        if (!checkCSRF(this, csrf)) return;

        this.body = '{}';
        this.status = 200;

        if (!this.session.a) {
            this.body = 'missing username';
            this.status = 500;
            return;
        }

        try {
            const res = yield api.signedCallAsync(
                'conveyor.get_tags_for_user',
                [this.session.a],
                config.get('conveyor_username'),
                config.get('conveyor_posting_wif')
            );

            this.body = JSON.stringify(res.includes(ACCEPTED_TOS_TAG));
        } catch (error) {
            console.error(
                'Error in /isTosAccepted api call',
                this.session.a,
                error
            );
            this.body = JSON.stringify({ error: error.message });
            this.status = 500;
        }
    });

    router.post('/acceptTos', koaBody, function*() {
        const params = this.request.body;
        const { csrf } = _parse(params);
        if (!checkCSRF(this, csrf)) return;

        if (!this.session.a) {
            this.body = 'missing logged in account';
            this.status = 500;
            return;
        }
        try {
            yield api.signedCallAsync(
                'conveyor.assign_tag',
                {
                    uid: this.session.a,
                    tag: ACCEPTED_TOS_TAG,
                },
                config.get('conveyor_username'),
                config.get('conveyor_posting_wif')
            );
            this.body = JSON.stringify({ status: 'ok' });
        } catch (error) {
            console.error(
                'Error in /acceptTos api call',
                this.session.uid,
                error
            );
            this.body = JSON.stringify({ error: error.message });
            this.status = 500;
        }
    });
    router.post('/search', koaBody, function*() {
        const { csrf } =
            typeof this.request.body === 'string'
                ? JSON.parse(this.request.body)
                : this.request.body;
        if (!checkCSRF(this, csrf)) return;

        try {
            const params = JSON.parse(this.request.body);
            const elasticSearchService = config.get(
                'steem_elastic_search_endpoint'
            );

            let searchEndpoint = null;
            console.log(params.depth);
            // 回复
            if (params.depth === 1) {
                searchEndpoint = elasticSearchService.concat(
                    '/hive_replies/_search?scroll=1m'
                );
            } else if (params.depth === 2) {
                // 用户
                searchEndpoint = elasticSearchService.concat(
                    '/hive_accounts/_search?scroll=1m'
                );
            } else {
                // 帖子
                searchEndpoint = elasticSearchService.concat(
                    '/hive_posts/_search?scroll=1m'
                );
            }

            let searchPayload = JSON.stringify(params.searchQuery);

            if (params.scrollQuery) {
                searchEndpoint = elasticSearchService.concat('/_search/scroll');
                searchPayload = JSON.stringify(params.scrollQuery);
            }

            const req = {
                method: this.request.method,
                headers: {
                    'Content-type': 'application/json',
                },
                body: searchPayload,
            };

            const searchResult = yield fetch(searchEndpoint, req);
            const resultJson = yield searchResult.json();
            this.body = JSON.stringify(resultJson);
            this.status = 200;
        } catch (error) {
            console.error('Error in /search api call', this.session.uid, error);
            this.body = JSON.stringify({ error: error.message });
            this.status = 500;
        }
    });
    router.get('/test_steem_market', function*() {
        const timepoints = [
            { timepoint: '2020-08-18T12:00:00Z', price_usd: '12250.0889773' },
            { timepoint: '2020-08-18T13:00:00Z', price_usd: '12260.3148296' },
            { timepoint: '2020-08-18T14:00:00Z', price_usd: '12167.8228289' },
            { timepoint: '2020-08-18T15:00:00Z', price_usd: '12005.0074994' },
            { timepoint: '2020-08-18T16:00:00Z', price_usd: '11986.8350721' },
            { timepoint: '2020-08-18T17:00:00Z', price_usd: '12036.3091798' },
            { timepoint: '2020-08-18T18:00:00Z', price_usd: '12028.9378102' },
            { timepoint: '2020-08-18T19:00:00Z', price_usd: '12003.137178' },
            { timepoint: '2020-08-18T20:00:00Z', price_usd: '12008.4364642' },
            { timepoint: '2020-08-18T21:00:00Z', price_usd: '12023.6575825' },
            { timepoint: '2020-08-18T22:00:00Z', price_usd: '12076.2749952' },
            { timepoint: '2020-08-18T23:00:00Z', price_usd: '12067.4046651' },
            { timepoint: '2020-08-19T00:00:00Z', price_usd: '11983.2200043' },
            { timepoint: '2020-08-19T01:00:00Z', price_usd: '12006.2852368' },
            { timepoint: '2020-08-19T02:00:00Z', price_usd: '11996.8557859' },
            { timepoint: '2020-08-19T03:00:00Z', price_usd: '11953.2549565' },
            { timepoint: '2020-08-19T04:00:00Z', price_usd: '11897.2228038' },
            { timepoint: '2020-08-19T05:00:00Z', price_usd: '11809.8239793' },
            { timepoint: '2020-08-19T06:00:00Z', price_usd: '11779.2416125' },
            { timepoint: '2020-08-19T07:00:00Z', price_usd: '11751.5085188' },
            { timepoint: '2020-08-19T08:00:00Z', price_usd: '11845.9507192' },
            { timepoint: '2020-08-19T09:00:00Z', price_usd: '11833.3444898' },
            { timepoint: '2020-08-19T10:00:00Z', price_usd: '11831.8872932' },
            { timepoint: '2020-08-19T11:00:00Z', price_usd: '11817.9327232' },
        ];
        const data = {
            top_coins: [
                {
                    symbol: 'BTC',
                    name: 'Bitcoin',
                    timepoints,
                },
                {
                    symbol: 'ETH',
                    name: 'Ethereum',
                    timepoints,
                },
                {
                    symbol: 'XRP',
                    name: 'XRP',
                    timepoints,
                },
            ],
            steem: {
                symbol: 'STEEM',
                name: 'Steem',
                timepoints,
            },
            sbd: {
                symbol: 'STEEM',
                name: 'Steem',
                timepoints,
            },
            tron: {
                symbol: 'TRX',
                name: 'Tron',
                timepoints,
            },
            jst: {
                symbol: 'JST',
                name: 'JUST',
                timepoints,
            },
        };
        this.body = JSON.stringify(data);
    });
    router.get('/create_account', function*() {
        const response = yield fetch(walletApiURI + '/create_account');
        const body = yield response.json();
        this.body = JSON.stringify(body);
    });

    router.get('/get_account', function*() {
        const q = this.request.query;
        if (!q) {
            this.body = JSON.stringify({ error: 'need_params' });
            return;
        }
        const response = yield fetch(
            walletApiURI + '/get_account?tron_address=' + q.tron_address
        );
        const body = yield response.json();
        this.body = JSON.stringify(body);
    });
    router.get('/get_config', function*() {
        const q = this.request.query;
        if (!q) {
            this.body = JSON.stringify({ error: 'need_params' });
            return;
        }
        const response = yield fetch(walletApiURI + '/get_config');
        const body = yield response.json();
        this.body = JSON.stringify(body);
    });
    router.get('/tron_user', function*() {
        const q = this.request.query;
        if (!q) {
            this.body = JSON.stringify({ error: 'need_params' });
            return;
        }
        const response = yield fetch(
            walletApiURI + '/tron_user?username=' + q.username
        );
        const body = yield response.json();
        this.body = JSON.stringify(body);
    });
    router.post('/tron_user', koaBody, function*() {
        const data =
            typeof this.request.body === 'string'
                ? JSON.parse(this.request.body)
                : this.request.body;
        if (typeof data !== 'object') {
            this.body = JSON.stringify({
                error: 'valid_input_data',
            });
            return;
        }
        if (data.username === undefined) {
            this.body = JSON.stringify({
                error: 'username_required',
            });
            return;
        }
        const request_base = {
            method: 'post',
            headers: {
                Accept: 'application/json',
                'Content-type': 'application/json',
            },
        };
        const request = Object.assign({}, request_base, {
            body: JSON.stringify(data),
        });
        const response = yield fetch(walletApiURI + '/tron_user', request);
        const body = yield response.json();
        this.body = JSON.stringify(body);
    });
}

const parseSig = hexSig => {
    try {
        return Signature.fromHex(hexSig);
    } catch (e) {
        return null;
    }
};


text:
package org.glowroot.microbenchmarks.support;

import org.glowroot.agent.plugin.api.Agent;
import org.glowroot.agent.plugin.api.MessageSupplier;
import org.glowroot.agent.plugin.api.OptionalThreadContext;
import org.glowroot.agent.plugin.api.TimerName;
import org.glowroot.agent.plugin.api.TraceEntry;
import org.glowroot.agent.plugin.api.weaving.BindThrowable;
import org.glowroot.agent.plugin.api.weaving.BindTraveler;
import org.glowroot.agent.plugin.api.weaving.OnBefore;
import org.glowroot.agent.plugin.api.weaving.OnReturn;
import org.glowroot.agent.plugin.api.weaving.OnThrow;
import org.glowroot.agent.plugin.api.weaving.Pointcut;

public class TransactionWorthyAspect {

    @Pointcut(className = "org.glowroot.microbenchmarks.core.support.TransactionWorthy",
            methodName = "doSomethingTransactionWorthy", methodParameterTypes = {},
            timerName = "transaction worthy")
    public static class TransactionWorthyAdvice {

        private static final TimerName timerName =
                Agent.getTimerName(TransactionWorthyAdvice.class);

        @OnBefore
        public static TraceEntry onBefore(OptionalThreadContext context) {
            return context.startTransaction("Microbenchmark", "transaction worthy",
                    MessageSupplier.create("transaction worthy"), timerName);
        }

        @OnReturn
        public static void onReturn(@BindTraveler TraceEntry traceEntry) {
            traceEntry.end();
        }

        @OnThrow
        public static void onThrow(@BindThrowable Throwable t,
                @BindTraveler TraceEntry traceEntry) {
            traceEntry.endWithError(t);
        }
    }
}


text:
@import "../../node_modules/normalize.css/normalize.css";
@import "../../node_modules/slick-carousel/slick/slick.css";
@import "../../node_modules/slick-carousel/slick/slick-theme.css";

* {
  -ms-box-sizing: border-box;
  -moz-box-sizing: border-box;
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
}
html {
  margin: 0;
  padding: 0;
}
body {
	background-color:white;
	color: #6c6868;
	min-height: 100vh;
}
#container {
	max-width: 1400px;
	margin: 0 auto;
}
.loader-inner > div {
	background-color: #6c6868;
}
.js #content {
	opacity: 1;
	transition: opacity 0.3s linear 0.1s;
}
#content {
	position: relative;
	padding: 20px 10px;
	min-height: calc(100vh - 159px);
}
.white {
	color:white;
}

#masthead > .main-nav { z-index:20; }
#masthead > .submenu { z-index:19; }
#masthead > .subnav { z-index:18; }
#masthead::after { z-index:17; }
#masthead { z-index: 6; }
#content { z-index:5; }
.page-background { z-index: 1; }

.modal {
	background-color: transparent;
	box-shadow: none;
	width: auto;
	-webkit-transform: scale(1) !important;
	-ms-transform: scale(1) !important;
	transform: scale(1) !important;
}
.loading.modal {
	background-color: transparent;
	bottom: 0;
	margin: auto;
	height: 20px;
	position: absolute;
	box-shadow: none;
}
.loader-inner {
	opacity: 0;
	transition: opacity 0.3s linear 0.3s;
	text-align: center;
}
.loading .loader-inner {
	opacity: 1;
}
.loader-outer {
	position: relative;
}
.loader-outer > .loader-inner,
.modal .loader-inner {
	position: absolute;
	top: 50%;
	left: 50%;
	z-index: 3;
}
.infinite-scroll__target {
	height: 200px;
	max-height: 20vw;
}

#content .text-content {
	position: relative;
	z-index: 3;
}
#content {
	font-size: 18px;
}
#content .page-content:not(.main-content),
#content .text-content:not(.page-content) p,
#content > p {
	max-width: 30em;
	margin-left: auto;
	margin-right: auto;
	padding: 1em;
}
.page-content {
	margin-left: auto;
	margin-right: auto;
}
#content .text-content p,
#content > p {
	line-height: 1.5;
}
#content .text-content p a:hover,
#content > p a:hover {
	color: black;
}
#content p:nth-of-type(1) {
	margin-top: 0;
}
#content p:nth-last-of-type(1) {
	margin-bottom: 0;
}
#content p.left {
	max-width: 500px;
	margin-left: -10px;
}
#content p > span {
	display:block;
}

#content img {
	max-width: 100%;
	outline-style: none;
	height: auto;
}

a {
	color: #d4d0d0;
	text-decoration: none;
	transition: color 0.1s linear;
}

@media (min-width: 670px) {
	#content {
		padding-top: 50px;
		padding-bottom: 50px;
	}
	.main-content.page-content,
	#content .text-content:not(.center) {
		margin-top:50px;
	}
}

body,
.background-canvas {
	background: white none center center no-repeat;
	background-size: cover;
}
body > .page-background {
	display: none;
}
@media (min-width: 768px) {
	.background-canvas > .background,
	.js .page-background {
		display: inline;
		visibility: hidden;
	}
}

.svg-icon {
	display: inline-block;
	-webkit-appearance: none;
	appearance: none;
}
.svg-icon svg {
	width: 90%;
	height: 90%;
	pointer-events: none;
}
.stroke { stroke: rgb(195,195,195); }
.fill { fill: rgb(195,195,195); }
.stroke:not(.fill) { fill: none; }
.fill:not(.stroke) { stroke: none; }
.icon--prev .next, .icon--next .prev { display: none; }
.fullscreen .icon--fullscreen .arrow {
	transform: rotate(180deg);
	transform-origin: 55% 45%;
}

figure {
	position: relative;
	margin: 0 auto;
}
figcaption > aside {
	display: none;
	font-family: "Arial", sans-serif;
	font-weight: bold;
	font-style: normal;
	position: absolute;
	text-transform: uppercase;
	left: 100%;
	bottom: 0;
	white-space: nowrap;
	font-size: 9px;
	letter-spacing: 1px;
	transform: rotate(-90deg) translate(2px, 12px);
	transform-origin: left bottom;
}
@media (min-width: 700px) {
	figcaption > aside {
		display: block;
	}
}
select {
	color: inherit;
	background-color: #ddd;
}
a.btn,
button,
input[type="submit"] {
	display: inline-block;
	outline-style: none;
	background-color: #ddd;
	font-weight: bold;
	border: 1px solid #ddd;
	text-decoration: none;
	color: inherit;
	text-transform: uppercase;
	padding: 0.5em 1em 0.4em;
	letter-spacing: 1px;
	-webkit-appearance: none;
	appearance: none;
}
a.btn.dark,
button.dark,
input[type="submit"].dark {
	background-color: #A9AAAC;
	border-color: #A9AAAC;
	color: white;
}
a.btn:hover,
button:hover {
	border-color: #444;
}
/*
a.btn.dark:hover,
button.dark:hover,
input[type="submit"].dark:hover {
	border-color: #444;
}
*/
input[type="submit"] {
	transition: color 0.1s linear, border-color 0.1s linear, background-color 0.1s linear;
}
.btn.loading,
input[type="submit"][disabled],
form.loading input[type="submit"] {
	background: white none center center no-repeat;
	border-color: transparent;
	color: #ddd;
	color: rgba(221,221,221,0.5);
}
.needs-empty-cart,
body[data-cart-count="0"] .needs-cart-items {
	display: none;
}
body[data-cart-count="0"] .needs-empty-cart {
	display: block;
}
.plural[data-count="1"] .plural__suffix,
.plural[data-count="1"] + .plural__suffix {
	display: none;
}