/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.client.admin.cli.util;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>
*/
public class ReturnFields implements Iterable<String> {
public static ReturnFields ALL = new ReturnFields() {
@Override
public ReturnFields child(String field) {
return NONE;
}
@Override
public boolean included(String... pathSegments) {
return true;
}
@Override
public boolean excluded(String field) {
return false;
}
@Override
public Iterator<String> iterator() {
return Collections.singletonList("*").iterator();
}
@Override
public boolean isEmpty() {
return false;
}
public boolean isAll() {
return true;
}
@Override
public String toString() {
return "[ReturnFields ALL]";
}
};
public static ReturnFields NONE = new ReturnFields() {
@Override
public ReturnFields child(String field) {
return this;
}
@Override
public boolean included(String... pathSegments) {
return false;
}
@Override
public boolean excluded(String field) {
return false;
}
@Override
public Iterator<String> iterator() {
List<String> emptyList = Collections.emptyList();
return emptyList.iterator();
}
@Override
public boolean isEmpty() {
return true;
}
@Override
public boolean isAll() {
return false;
}
@Override
public String toString() {
return "[ReturnFields NONE]";
}
};
public static ReturnFields ALL_RECURSIVELY = new ReturnFields() {
@Override
public ReturnFields child(String field) {
return this;
}
@Override
public boolean included(String... pathSegments) {
return true;
}
@Override
public boolean excluded(String field) {
return false;
}
@Override
public Iterator<String> iterator() {
List<String> emptyList = Collections.emptyList();
return emptyList.iterator();
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public boolean isAll() {
return true;
}
};
private enum TargetState {
IdentCommaOpen,
Ident,
Comma,
Anything
}
private enum FieldState {
start,
name,
end
}
private HashMap<String, ReturnFields> fields = new LinkedHashMap<>();
public ReturnFields() {}
public ReturnFields(String spec) {
if (spec == null || spec.trim().length() == 0) {
throw new IllegalArgumentException("Fields spec is null or empty!");
}
// parse the spec, building up the tree for nested children
char[] buf = spec.toCharArray();
StringBuilder token = new StringBuilder(buf.length);
// stack for handling depth
LinkedList<HashMap<String, ReturnFields>> specs = new LinkedList<>();
specs.add(fields);
// parser state
FieldState fldState = FieldState.start;
TargetState state = TargetState.Ident;
int i;
for (i = 0; i < buf.length; i++) {
char c = buf[i];
if (c == ',') {
if (state == TargetState.Ident) {
error(spec, i);
}
if (fldState == FieldState.name) {
specs.getLast().put(token.toString(), null);
token.setLength(0);
}
state = TargetState.Ident;
fldState = FieldState.start;
} else if (c == '(') {
if (state != TargetState.IdentCommaOpen && state != TargetState.Anything) {
error(spec, i);
}
ReturnFields sub = new ReturnFields();
specs.getLast().put(token.toString(), sub);
specs.add(sub.fields);
token.setLength(0);
state = TargetState.Ident;
fldState = FieldState.start;
} else if (c == ')') {
if (state != TargetState.Anything) {
error(spec, i);
}
if (fldState == FieldState.name) {
specs.getLast().put(token.toString(), null);
token.setLength(0);
}
specs.removeLast();
fldState = FieldState.end;
state = specs.size() > 1 ? TargetState.Anything : TargetState.Comma;
} else {
token.append(c);
if (fldState == FieldState.start) {
fldState = FieldState.name;
state = specs.size() > 1 ? TargetState.Anything : TargetState.IdentCommaOpen;
}
}
}
if (specs.size() > 1) {
error(spec, i);
}
if (token.length() > 0) {
specs.getLast().put(token.toString(), null);
} else if (!(state == TargetState.Anything || state == TargetState.Comma)) {
error(spec, i);
}
}
private void error(String spec, int i) {
throw new RuntimeException("Invalid fields specification at position " + i + ": " + spec);
}
/**
* Get ReturnFields for a child field of JSONObject type.
*
* <p>For basic-typed fields this always returns null. Use included() for those.</p>
*
* @param field The child field name for nested returns.
* @return ReturnFields for a child field
*/
public ReturnFields child(String field) {
ReturnFields returnFields = fields.get(field);
if (returnFields == null) {
returnFields = fields.get("*");
if (returnFields == null) {
returnFields = ReturnFields.NONE;
}
}
return returnFields;
}
/**
* Check to see if the field should be included in JSON response.
*
* <p>The check can be performed for any level of depth relative to current nesting level, by specifying multiple path segments.</p>
*
* @param pathSegments Segments to test in the tree of return fields.
* @return true if the specified path should be part of JSON response or not
*/
public boolean included(String... pathSegments) {
if (pathSegments == null || pathSegments.length == 0) {
throw new IllegalArgumentException("No path specified!");
}
ReturnFields current = this;
for (String path : pathSegments) {
if (current == null) {
return false;
}
if (current.fields.containsKey("-" + path)) {
return false;
}
if (current.fields.containsKey("*")) {
return true;
}
if (!current.fields.containsKey(path)) {
return false;
}
current = current.fields.get(path);
}
return true;
}
/**
* Check to see if the field specified is set to be explicitly excluded.
* @param field The field name to check
* @return If the field was explicitly set to be excluded
*/
public boolean excluded(String field) {
if (fields.containsKey("-" + field)) {
return true;
} else {
return false;
}
}
/**
* Iterate over child fields to be included in response.
*
* <p>To get nested field specifier use child(name) passing the field name this iterator returns.</p>
*
* @return iterator over child fields to be included in response.
*/
public Iterator<String> iterator() {
return fields.keySet().iterator();
}
/**
* Determine if zero fields should be returned.
*
* @return <code>true</code> if the list is empty, else, <code>false</code>
*/
public boolean isEmpty() {
return this.fields.isEmpty();
}
public boolean isAll() {
return this.fields.keySet().contains("*");
}
@Override
public String toString() {
return "[ReturnFieldsImpl: fields=" + this.fields + "]";
}
}