| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.solr.handler.admin; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.Reader; |
| import java.io.Writer; |
| import java.lang.invoke.MethodHandles; |
| import java.net.URLEncoder; |
| import java.nio.charset.StandardCharsets; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.SortedMap; |
| import java.util.TreeMap; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.apache.lucene.util.BytesRef; |
| import org.apache.solr.cloud.ZkController; |
| import org.apache.solr.common.SolrException; |
| import org.apache.solr.common.SolrException.ErrorCode; |
| import org.apache.solr.common.cloud.OnReconnect; |
| import org.apache.solr.common.cloud.Replica; |
| import org.apache.solr.common.cloud.SolrZkClient; |
| import org.apache.solr.common.cloud.ZkStateReader; |
| import org.apache.solr.common.params.MapSolrParams; |
| import org.apache.solr.common.params.SolrParams; |
| import org.apache.solr.common.util.ContentStream; |
| import org.apache.solr.common.util.Utils; |
| import org.apache.solr.core.CoreContainer; |
| import org.apache.solr.handler.RequestHandlerBase; |
| import org.apache.solr.request.SolrQueryRequest; |
| import org.apache.solr.response.JSONResponseWriter; |
| import org.apache.solr.response.RawResponseWriter; |
| import org.apache.solr.response.SolrQueryResponse; |
| import org.apache.solr.util.SimplePostTool.BAOS; |
| import org.apache.zookeeper.KeeperException; |
| import org.apache.zookeeper.WatchedEvent; |
| import org.apache.zookeeper.Watcher; |
| import org.apache.zookeeper.data.Stat; |
| import org.apache.zookeeper.server.ByteBufferInputStream; |
| import org.noggit.CharArr; |
| import org.noggit.JSONWriter; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import static org.apache.solr.common.params.CommonParams.OMIT_HEADER; |
| import static org.apache.solr.common.params.CommonParams.PATH; |
| import static org.apache.solr.common.params.CommonParams.WT; |
| |
| |
| /** |
| * Zookeeper Info |
| * |
| * @since solr 4.0 |
| */ |
| public final class ZookeeperInfoHandler extends RequestHandlerBase { |
| private final CoreContainer cores; |
| |
| private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); |
| |
| // used for custom sorting collection names looking like prefix## |
| // only go out to 7 digits (which safely fits in an int) |
| private static final Pattern endsWithDigits = Pattern.compile("^(\\D*)(\\d{1,7}?)$"); |
| |
| public ZookeeperInfoHandler(CoreContainer cc) { |
| this.cores = cc; |
| } |
| |
| |
| @Override |
| public String getDescription() { |
| return "Fetch Zookeeper contents"; |
| } |
| |
| @Override |
| public Category getCategory() { |
| return Category.ADMIN; |
| } |
| |
| /** |
| * Enumeration of ways to filter collections on the graph panel. |
| */ |
| static enum FilterType { |
| none, name, status |
| } |
| |
| /** |
| * Holds state of a single page of collections requested from the cloud panel. |
| */ |
| static final class PageOfCollections { |
| List<String> selected; |
| int numFound = 0; // total number of matches (across all pages) |
| int start = 0; |
| int rows = -1; |
| FilterType filterType; |
| String filter; |
| |
| PageOfCollections(int start, int rows, FilterType filterType, String filter) { |
| this.start = start; |
| this.rows = rows; |
| this.filterType = filterType; |
| this.filter = filter; |
| } |
| |
| void selectPage(List<String> collections) { |
| numFound = collections.size(); |
| // start with full set and then find the sublist for the desired selected |
| selected = collections; |
| |
| if (rows > 0) { // paging desired |
| if (start > numFound) |
| start = 0; // this might happen if they applied a new filter |
| |
| int lastIndex = Math.min(start + rows, numFound); |
| if (start > 0 || lastIndex < numFound) |
| selected = collections.subList(start, lastIndex); |
| } |
| } |
| |
| /** |
| * Filters a list of collections by name if applicable. |
| */ |
| List<String> applyNameFilter(List<String> collections) { |
| |
| if (filterType != FilterType.name || filter == null) |
| return collections; // name filter doesn't apply |
| |
| // typically, a user will type a prefix and then *, e.g. tj* |
| // when they really mean tj.* |
| String regexFilter = (!filter.endsWith(".*") && filter.endsWith("*")) |
| ? filter.substring(0, filter.length() - 1) + ".*" : filter; |
| |
| // case-insensitive |
| if (!regexFilter.startsWith("(?i)")) |
| regexFilter = "(?i)" + regexFilter; |
| |
| Pattern filterRegex = Pattern.compile(regexFilter); |
| List<String> filtered = new ArrayList<String>(); |
| for (String next : collections) { |
| if (matches(filterRegex, next)) |
| filtered.add(next); |
| } |
| |
| return filtered; |
| } |
| |
| /** |
| * Walk the collection state JSON object to see if it has any replicas that match |
| * the state the user is filtering by. |
| */ |
| @SuppressWarnings("unchecked") |
| final boolean matchesStatusFilter(Map<String, Object> collectionState, Set<String> liveNodes) { |
| |
| if (filterType != FilterType.status || filter == null || filter.length() == 0) |
| return true; // no status filter, so all match |
| |
| boolean isHealthy = true; // means all replicas for all shards active |
| boolean hasDownedShard = false; // means one or more shards is down |
| boolean replicaInRecovery = false; |
| |
| Map<String, Object> shards = (Map<String, Object>) collectionState.get("shards"); |
| for (Object o : shards.values()) { |
| boolean hasActive = false; |
| Map<String, Object> shard = (Map<String, Object>) o; |
| Map<String, Object> replicas = (Map<String, Object>) shard.get("replicas"); |
| for (Object value : replicas.values()) { |
| Map<String, Object> replicaState = (Map<String, Object>) value; |
| Replica.State coreState = Replica.State.getState((String) replicaState.get(ZkStateReader.STATE_PROP)); |
| String nodeName = (String) replicaState.get("node_name"); |
| |
| // state can lie to you if the node is offline, so need to reconcile with live_nodes too |
| if (!liveNodes.contains(nodeName)) |
| coreState = Replica.State.DOWN; // not on a live node, so must be down |
| |
| if (coreState == Replica.State.ACTIVE) { |
| hasActive = true; // assumed no replicas active and found one that is for this shard |
| } else { |
| if (coreState == Replica.State.RECOVERING) { |
| replicaInRecovery = true; |
| } |
| isHealthy = false; // assumed healthy and found one replica that is not |
| } |
| } |
| |
| if (!hasActive) |
| hasDownedShard = true; // this is bad |
| } |
| |
| if ("healthy".equals(filter)) { |
| return isHealthy; |
| } else if ("degraded".equals(filter)) { |
| return !hasDownedShard && !isHealthy; // means no shards offline but not 100% healthy either |
| } else if ("downed_shard".equals(filter)) { |
| return hasDownedShard; |
| } else if (Replica.State.getState(filter) == Replica.State.RECOVERING) { |
| return !isHealthy && replicaInRecovery; |
| } |
| |
| return true; |
| } |
| |
| final boolean matches(final Pattern filter, final String collName) { |
| return filter.matcher(collName).matches(); |
| } |
| |
| String getPagingHeader() { |
| return start + "|" + rows + "|" + numFound + "|" + (filterType != null ? filterType.toString() : "") + "|" + (filter != null ? filter : ""); |
| } |
| |
| public String toString() { |
| return getPagingHeader(); |
| } |
| |
| } |
| |
| /** |
| * Supports paged navigation of collections on the cloud panel. To avoid serving |
| * stale collection data, this object watches the /collections znode, which will |
| * change if a collection is added or removed. |
| */ |
| static final class PagedCollectionSupport implements Watcher, Comparator<String>, OnReconnect { |
| |
| // this is the full merged list of collections from ZooKeeper |
| private List<String> cachedCollections; |
| |
| /** |
| * If the list of collections changes, mark the cache as stale. |
| */ |
| @Override |
| public void process(WatchedEvent event) { |
| // session events are not change events, and do not remove the watcher |
| if (Event.EventType.None.equals(event.getType())) { |
| return; |
| } |
| synchronized (this) { |
| cachedCollections = null; |
| } |
| } |
| |
| /** |
| * Create a merged view of all collections from /collections/?/state.json |
| */ |
| private synchronized List<String> getCollections(SolrZkClient zkClient) throws KeeperException, InterruptedException { |
| if (cachedCollections == null) { |
| // cache is stale, rebuild the full list ... |
| cachedCollections = new ArrayList<String>(); |
| |
| List<String> fromZk = zkClient.getChildren("/collections", this, true); |
| if (fromZk != null) |
| cachedCollections.addAll(fromZk); |
| |
| // sort the final merged set of collections |
| Collections.sort(cachedCollections, this); |
| } |
| |
| return cachedCollections; |
| } |
| |
| /** |
| * Gets the requested page of collections after applying filters and offsets. |
| */ |
| public void fetchPage(PageOfCollections page, SolrZkClient zkClient) |
| throws KeeperException, InterruptedException { |
| |
| |
| List<String> children = getCollections(zkClient); |
| page.selected = children; // start with the page being the full list |
| |
| // activate paging (if disabled) for large collection sets |
| if (page.start == 0 && page.rows == -1 && page.filter == null && children.size() > 10) { |
| page.rows = 20; |
| page.start = 0; |
| } |
| |
| // apply the name filter if supplied (we don't need to pull state |
| // data from ZK to do name filtering |
| if (page.filterType == FilterType.name && page.filter != null) |
| children = page.applyNameFilter(children); |
| |
| // a little hacky ... we can't select the page when filtering by |
| // status until reading all status objects from ZK |
| if (page.filterType != FilterType.status) |
| page.selectPage(children); |
| } |
| |
| @Override |
| public int compare(String left, String right) { |
| if (left == null) |
| return -1; |
| |
| if (left.equals(right)) |
| return 0; |
| |
| // sort lexically unless the two collection names start with the same base prefix |
| // and end in a number (which is a common enough naming scheme to have direct |
| // support for it) |
| Matcher leftMatcher = endsWithDigits.matcher(left); |
| if (leftMatcher.matches()) { |
| Matcher rightMatcher = endsWithDigits.matcher(right); |
| if (rightMatcher.matches()) { |
| String leftGroup1 = leftMatcher.group(1); |
| String rightGroup1 = rightMatcher.group(1); |
| if (leftGroup1.equals(rightGroup1)) { |
| // both start with the same prefix ... compare indexes |
| // using longs here as we don't know how long the 2nd group is |
| int leftGroup2 = Integer.parseInt(leftMatcher.group(2)); |
| int rightGroup2 = Integer.parseInt(rightMatcher.group(2)); |
| return (leftGroup2 > rightGroup2) ? 1 : ((leftGroup2 == rightGroup2) ? 0 : -1); |
| } |
| } |
| } |
| return left.compareTo(right); |
| } |
| |
| /** |
| * Called after a ZooKeeper session expiration occurs |
| */ |
| @Override |
| public void command() { |
| // we need to re-establish the watcher on the collections list after session expires |
| synchronized (this) { |
| cachedCollections = null; |
| } |
| } |
| } |
| |
| private PagedCollectionSupport pagingSupport; |
| |
| @Override |
| @SuppressWarnings({"unchecked"}) |
| public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception { |
| final SolrParams params = req.getParams(); |
| Map<String, String> map = new HashMap<>(1); |
| map.put(WT, "raw"); |
| map.put(OMIT_HEADER, "true"); |
| req.setParams(SolrParams.wrapDefaults(new MapSolrParams(map), params)); |
| synchronized (this) { |
| if (pagingSupport == null) { |
| pagingSupport = new PagedCollectionSupport(); |
| ZkController zkController = cores.getZkController(); |
| if (zkController != null) { |
| // get notified when the ZK session expires (so we can clear the cached collections and rebuild) |
| zkController.addOnReconnectListener(pagingSupport); |
| } |
| } |
| } |
| |
| String path = params.get(PATH); |
| |
| if (params.get("addr") != null) { |
| throw new SolrException(ErrorCode.BAD_REQUEST, "Illegal parameter \"addr\""); |
| } |
| |
| String detailS = params.get("detail"); |
| boolean detail = detailS != null && detailS.equals("true"); |
| |
| String dumpS = params.get("dump"); |
| boolean dump = dumpS != null && dumpS.equals("true"); |
| |
| int start = params.getInt("start", 0); // Note start ignored if rows not specified |
| int rows = params.getInt("rows", -1); |
| |
| String filterType = params.get("filterType"); |
| if (filterType != null) { |
| filterType = filterType.trim().toLowerCase(Locale.ROOT); |
| if (filterType.length() == 0) |
| filterType = null; |
| } |
| FilterType type = (filterType != null) ? FilterType.valueOf(filterType) : FilterType.none; |
| |
| String filter = (type != FilterType.none) ? params.get("filter") : null; |
| if (filter != null) { |
| filter = filter.trim(); |
| if (filter.length() == 0) |
| filter = null; |
| } |
| |
| ZKPrinter printer = new ZKPrinter(cores.getZkController()); |
| printer.detail = detail; |
| printer.dump = dump; |
| boolean isGraphView = "graph".equals(params.get("view")); |
| // There is no znode /clusterstate.json (removed in Solr 9), but we do as if there's one and return collection listing |
| // Need to change services.js if cleaning up here, collection list is used from Admin UI Cloud - Graph |
| boolean paginateCollections = (isGraphView && "/clusterstate.json".equals(path)); |
| printer.page = paginateCollections ? new PageOfCollections(start, rows, type, filter) : null; |
| printer.pagingSupport = pagingSupport; |
| |
| try { |
| if (paginateCollections) { |
| // List collections and allow pagination, but no specific znode info like when looking at a normal ZK path |
| printer.printPaginatedCollections(); |
| } else { |
| printer.print(path); |
| } |
| } finally { |
| printer.close(); |
| } |
| rsp.getValues().add(RawResponseWriter.CONTENT,printer); |
| } |
| |
| //-------------------------------------------------------------------------------------- |
| // |
| //-------------------------------------------------------------------------------------- |
| |
| static class ZKPrinter implements ContentStream { |
| static boolean FULLPATH_DEFAULT = false; |
| |
| boolean indent = true; |
| boolean fullpath = FULLPATH_DEFAULT; |
| boolean detail = false; |
| boolean dump = false; |
| |
| String keeperAddr; // the address we're connected to |
| |
| final BAOS baos = new BAOS(); |
| final Writer out = new OutputStreamWriter(baos, StandardCharsets.UTF_8); |
| SolrZkClient zkClient; |
| |
| PageOfCollections page; |
| PagedCollectionSupport pagingSupport; |
| ZkController zkController; |
| |
| public ZKPrinter(ZkController controller) throws IOException { |
| this.zkController = controller; |
| keeperAddr = controller.getZkServerAddress(); |
| zkClient = controller.getZkClient(); |
| } |
| |
| public void close() { |
| try { |
| out.flush(); |
| } catch (Exception e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| // main entry point for printing from path |
| void print(String path) throws IOException { |
| if (zkClient == null) { |
| return; |
| } |
| |
| // normalize path |
| if (path == null) { |
| path = "/"; |
| } else { |
| path = path.trim(); |
| if (path.length() == 0) { |
| path = "/"; |
| } |
| } |
| |
| if (path.endsWith("/") && path.length() > 1) { |
| path = path.substring(0, path.length() - 1); |
| } |
| |
| int idx = path.lastIndexOf('/'); |
| String parent = idx >= 0 ? path.substring(0, idx) : path; |
| if (parent.length() == 0) { |
| parent = "/"; |
| } |
| |
| CharArr chars = new CharArr(); |
| JSONWriter json = new JSONWriter(chars, 2); |
| json.startObject(); |
| |
| if (detail) { |
| if (!printZnode(json, path)) { |
| return; |
| } |
| json.writeValueSeparator(); |
| } |
| |
| json.writeString("tree"); |
| json.writeNameSeparator(); |
| json.startArray(); |
| if (!printTree(json, path)) { |
| return; // there was an error |
| } |
| json.endArray(); |
| json.endObject(); |
| out.write(chars.toString()); |
| } |
| |
| // main entry point for printing collections |
| @SuppressWarnings("unchecked") |
| void printPaginatedCollections() throws IOException { |
| SortedMap<String, Object> collectionStates; |
| try { |
| // support paging of the collections graph view (in case there are many collections) |
| // fetch the requested page of collections and then retrieve the state for each |
| pagingSupport.fetchPage(page, zkClient); |
| // keep track of how many collections match the filter |
| boolean applyStatusFilter = (page.filterType == FilterType.status && page.filter != null); |
| List<String> matchesStatusFilter = applyStatusFilter ? new ArrayList<String>() : null; |
| Set<String> liveNodes = applyStatusFilter ? |
| zkController.getZkStateReader().getClusterState().getLiveNodes() : null; |
| |
| collectionStates = new TreeMap<>(pagingSupport); |
| for (String collection : page.selected) { |
| // Get collection state from ZK |
| String collStatePath = String.format(Locale.ROOT, "/collections/%s/state.json", collection); |
| String childDataStr = null; |
| try { |
| byte[] childData = zkClient.getData(collStatePath, null, null, true); |
| if (childData != null) |
| childDataStr = (new BytesRef(childData)).utf8ToString(); |
| } catch (KeeperException.NoNodeException nne) { |
| log.warn("State for collection {} not found.", collection); |
| } catch (Exception childErr) { |
| log.error("Failed to get {} due to", collStatePath, childErr); |
| } |
| |
| if (childDataStr != null) { |
| Map<String, Object> extColl = (Map<String, Object>) Utils.fromJSONString(childDataStr); |
| Object collectionState = extColl.get(collection); |
| |
| if (applyStatusFilter) { |
| // verify this collection matches the filtered state |
| if (page.matchesStatusFilter((Map<String, Object>) collectionState, liveNodes)) { |
| matchesStatusFilter.add(collection); |
| collectionStates.put(collection, collectionState); |
| } |
| } else { |
| collectionStates.put(collection, collectionState); |
| } |
| } |
| } |
| |
| if (applyStatusFilter) { |
| // update the paged navigation info after applying the status filter |
| page.selectPage(matchesStatusFilter); |
| |
| // rebuild the Map of state data |
| SortedMap<String, Object> map = new TreeMap<String, Object>(pagingSupport); |
| for (String next : page.selected) |
| map.put(next, collectionStates.get(next)); |
| collectionStates = map; |
| } |
| } catch (KeeperException | InterruptedException e) { |
| writeError(500, e.toString()); |
| return; |
| } |
| |
| CharArr chars = new CharArr(); |
| JSONWriter json = new JSONWriter(chars, 2); |
| json.startObject(); |
| |
| json.writeString("znode"); |
| json.writeNameSeparator(); |
| json.startObject(); |
| |
| // For some reason, without this the Json is badly formed |
| writeKeyValue(json, PATH, "Undefined", true); |
| |
| if (collectionStates != null) { |
| CharArr collectionOut = new CharArr(); |
| new JSONWriter(collectionOut, 2).write(collectionStates); |
| writeKeyValue(json, "data", collectionOut.toString(), false); |
| } |
| |
| writeKeyValue(json, "paging", page.getPagingHeader(), false); |
| |
| json.endObject(); |
| json.endObject(); |
| out.write(chars.toString()); |
| } |
| |
| void writeError(int code, String msg) throws IOException { |
| throw new SolrException(ErrorCode.getErrorCode(code), msg); |
| /*response.setStatus(code); |
| |
| CharArr chars = new CharArr(); |
| JSONWriter w = new JSONWriter(chars, 2); |
| w.startObject(); |
| w.indent(); |
| w.writeString("status"); |
| w.writeNameSeparator(); |
| w.write(code); |
| w.writeValueSeparator(); |
| w.indent(); |
| w.writeString("error"); |
| w.writeNameSeparator(); |
| w.writeString(msg); |
| w.endObject(); |
| |
| out.write(chars.toString());*/ |
| } |
| |
| boolean printTree(JSONWriter json, String path) throws IOException { |
| String label = path; |
| if (!fullpath) { |
| int idx = path.lastIndexOf('/'); |
| label = idx > 0 ? path.substring(idx + 1) : path; |
| } |
| json.startObject(); |
| writeKeyValue(json, "text", label, true); |
| json.writeValueSeparator(); |
| json.writeString("a_attr"); |
| json.writeNameSeparator(); |
| json.startObject(); |
| String href = "admin/zookeeper?detail=true&path=" + URLEncoder.encode(path, StandardCharsets.UTF_8); |
| writeKeyValue(json, "href", href, true); |
| json.endObject(); |
| |
| Stat stat = new Stat(); |
| try { |
| // Trickily, the call to zkClient.getData fills in the stat variable |
| byte[] data = zkClient.getData(path, null, stat, true); |
| |
| if (stat.getEphemeralOwner() != 0) { |
| writeKeyValue(json, "ephemeral", true, false); |
| writeKeyValue(json, "version", stat.getVersion(), false); |
| } |
| |
| if (dump) { |
| json.writeValueSeparator(); |
| printZnode(json, path); |
| } |
| |
| } catch (IllegalArgumentException e) { |
| // path doesn't exist (must have been removed) |
| writeKeyValue(json, "warning", "(path gone)", false); |
| } catch (KeeperException e) { |
| writeKeyValue(json, "warning", e.toString(), false); |
| log.warn("Keeper Exception", e); |
| } catch (InterruptedException e) { |
| writeKeyValue(json, "warning", e.toString(), false); |
| log.warn("InterruptedException", e); |
| } |
| |
| if (stat.getNumChildren() > 0) { |
| json.writeValueSeparator(); |
| if (indent) { |
| json.indent(); |
| } |
| json.writeString("children"); |
| json.writeNameSeparator(); |
| json.startArray(); |
| |
| try { |
| List<String> children = zkClient.getChildren(path, null, true); |
| java.util.Collections.sort(children); |
| |
| boolean first = true; |
| for (String child : children) { |
| if (!first) { |
| json.writeValueSeparator(); |
| } |
| |
| String childPath = path + (path.endsWith("/") ? "" : "/") + child; |
| if (!printTree(json, childPath)) { |
| return false; |
| } |
| first = false; |
| } |
| } catch (KeeperException e) { |
| writeError(500, e.toString()); |
| return false; |
| } catch (InterruptedException e) { |
| writeError(500, e.toString()); |
| return false; |
| } catch (IllegalArgumentException e) { |
| // path doesn't exist (must have been removed) |
| json.writeString("(children gone)"); |
| } |
| |
| json.endArray(); |
| } |
| |
| json.endObject(); |
| return true; |
| } |
| |
| String time(long ms) { |
| return (new Date(ms)).toString() + " (" + ms + ")"; |
| } |
| |
| public void writeKeyValue(JSONWriter json, String k, Object v, boolean isFirst) { |
| if (!isFirst) { |
| json.writeValueSeparator(); |
| } |
| if (indent) { |
| json.indent(); |
| } |
| json.writeString(k); |
| json.writeNameSeparator(); |
| json.write(v); |
| } |
| |
| boolean printZnode(JSONWriter json, String path) throws IOException { |
| try { |
| String dataStr = null; |
| String dataStrErr = null; |
| Stat stat = new Stat(); |
| // Trickily, the call to zkClient.getData fills in the stat variable |
| byte[] data = zkClient.getData(path, null, stat, true); |
| if (null != data) { |
| try { |
| dataStr = (new BytesRef(data)).utf8ToString(); |
| } catch (Exception e) { |
| dataStrErr = "data is not parsable as a utf8 String: " + e.toString(); |
| } |
| } |
| |
| json.writeString("znode"); |
| json.writeNameSeparator(); |
| json.startObject(); |
| |
| writeKeyValue(json, PATH, path, true); |
| |
| json.writeValueSeparator(); |
| json.writeString("prop"); |
| json.writeNameSeparator(); |
| json.startObject(); |
| writeKeyValue(json, "version", stat.getVersion(), true); |
| writeKeyValue(json, "aversion", stat.getAversion(), false); |
| writeKeyValue(json, "children_count", stat.getNumChildren(), false); |
| writeKeyValue(json, "ctime", time(stat.getCtime()), false); |
| writeKeyValue(json, "cversion", stat.getCversion(), false); |
| writeKeyValue(json, "czxid", stat.getCzxid(), false); |
| writeKeyValue(json, "ephemeralOwner", stat.getEphemeralOwner(), false); |
| writeKeyValue(json, "mtime", time(stat.getMtime()), false); |
| writeKeyValue(json, "mzxid", stat.getMzxid(), false); |
| writeKeyValue(json, "pzxid", stat.getPzxid(), false); |
| writeKeyValue(json, "dataLength", stat.getDataLength(), false); |
| if (null != dataStrErr) { |
| writeKeyValue(json, "dataNote", dataStrErr, false); |
| } |
| json.endObject(); |
| |
| if (null != dataStr) { |
| writeKeyValue(json, "data", dataStr, false); |
| } |
| |
| if (page != null) { |
| writeKeyValue(json, "paging", page.getPagingHeader(), false); |
| } |
| |
| json.endObject(); |
| } catch (KeeperException e) { |
| writeError(500, e.toString()); |
| return false; |
| } catch (InterruptedException e) { |
| writeError(500, e.toString()); |
| return false; |
| } |
| return true; |
| } |
| |
| /* @Override |
| public void write(OutputStream os) throws IOException { |
| ByteBuffer bytes = baos.getByteBuffer(); |
| os.write(bytes.array(),0,bytes.limit()); |
| } |
| */ |
| @Override |
| public String getName() { |
| return null; |
| } |
| |
| @Override |
| public String getSourceInfo() { |
| return null; |
| } |
| |
| @Override |
| public String getContentType() { |
| return JSONResponseWriter.CONTENT_TYPE_JSON_UTF8; |
| } |
| |
| @Override |
| public Long getSize() { |
| return null; |
| } |
| |
| @Override |
| public InputStream getStream() throws IOException { |
| return new ByteBufferInputStream(baos.getByteBuffer()); |
| } |
| |
| @Override |
| public Reader getReader() throws IOException { |
| return null; |
| } |
| } |
| } |