class UsersMetadata extends AbstractNamedDiffable<Metadata.Custom> implements Metadata.Custom {

    public static final String TYPE = "users";

    private final Map<String, SecureHash> users;

    public UsersMetadata() {
        this.users = new HashMap<>();
    }

    public UsersMetadata(Map<String, SecureHash> users) {
        this.users = users;
    }

    public static UsersMetadata newInstance(@Nullable UsersMetadata instance) {
        if (instance == null) {
            return new UsersMetadata();
        }
        return new UsersMetadata(new HashMap<>(instance.users));
    }

    public boolean contains(String name) {
        return users.containsKey(name);
    }

    public void put(String name, @Nullable SecureHash secureHash) {
        users.put(name, secureHash);
    }

    public void remove(String name) {
        users.remove(name);
    }

    public List<String> userNames() {
        return new ArrayList<>(users.keySet());
    }

    public Map<String, SecureHash> users() {
        return users;
    }

    public UsersMetadata(StreamInput in) throws IOException {
        int numUsers = in.readVInt();
        users = new HashMap<>(numUsers);
        for (int i = 0; i < numUsers; i++) {
            String userName = in.readString();
            SecureHash secureHash = in.readOptionalWriteable(SecureHash::readFrom);
            users.put(userName, secureHash);
        }
    }

    @Override
    public void writeTo(StreamOutput out) throws IOException {
        out.writeVInt(users.size());
        for (Map.Entry<String, SecureHash> user : users.entrySet()) {
            out.writeString(user.getKey());
            out.writeOptionalWriteable(user.getValue());
        }
    }

    @Override
    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
        builder.startObject("users");
        for (Map.Entry<String, SecureHash> entry : users.entrySet()) {
            builder.startObject(entry.getKey());
            if (entry.getValue() != null) {
                entry.getValue().toXContent(builder, params);
            }
            builder.endObject();
        }
        builder.endObject();
        return builder;
    }

    /**
     * UsersMetadata has the form of:
     *
     * users: {
     *   "user1": {
     *     "secure_hash": {
     *       "iterations": INT,
     *       "hash": BYTE[],
     *       "salt": BYTE[]
     *     }
     *   },
     *   "user2": {
     *     "secure_hash": null
     *   },
     *   ...
     * }
     */
    public static UsersMetadata fromXContent(XContentParser parser) throws IOException {<FILL_FUNCTION_BODY>}

    @Override
    public EnumSet<Metadata.XContentContext> context() {
        return EnumSet.of(Metadata.XContentContext.GATEWAY, Metadata.XContentContext.SNAPSHOT);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        UsersMetadata that = (UsersMetadata) o;
        return users.equals(that.users);
    }

    @Override
    public int hashCode() {
        return Objects.hash(users);
    }

    @Override
    public String getWriteableName() {
        return TYPE;
    }

    @Override
    public Version getMinimalSupportedVersion() {
        return Version.V_3_0_1;
    }
}

        Map<String, SecureHash> users = new HashMap<>();
        XContentParser.Token token = parser.nextToken();

        if (token == XContentParser.Token.FIELD_NAME && parser.currentName().equals(TYPE)) {
            token = parser.nextToken();
            if (token == XContentParser.Token.START_OBJECT) {
                while (parser.nextToken() == XContentParser.Token.FIELD_NAME) {
                    String userName = parser.currentName();
                    SecureHash secureHash = null;
                    if (parser.nextToken() == XContentParser.Token.START_OBJECT) {
                        while (parser.nextToken() == XContentParser.Token.FIELD_NAME) {
                            if (parser.currentName().equals("secure_hash")) {
                                secureHash = SecureHash.fromXContent(parser);
                            } else {
                                throw new ElasticsearchParseException(
                                        "failed to parse users, unexpected field name: " + parser.currentName()
                                );
                            }
                        }
                        if (parser.currentToken() != XContentParser.Token.END_OBJECT) {
                            throw new ElasticsearchParseException(
                                "failed to parse users, expected an object token at the end, got: " + parser.currentToken()
                            );
                        }
                    }
                    users.put(userName, secureHash);
                }
            } else {
                // each custom metadata is packed inside an object.
                throw new ElasticsearchParseException("failed to parse users, expected an object token at start");
            }
            if (parser.nextToken() != XContentParser.Token.END_OBJECT) {
                // each custom metadata is packed inside an object.
                // each custom must move the parser to the end otherwise possible following customs won't be read
                throw new ElasticsearchParseException("failed to parse users, expected an object token at the end");
            }
        }
        return new UsersMetadata(users);
    
<methods>public non-sealed void <init>() ,public Diff<org.elasticsearch.cluster.metadata.Metadata.Custom> diff(org.elasticsearch.cluster.metadata.Metadata.Custom) ,public org.elasticsearch.cluster.metadata.Metadata.Custom get() ,public static NamedDiff<T> readDiffFrom(Class<? extends T>, java.lang.String, org.elasticsearch.common.io.stream.StreamInput) throws java.io.IOException<variables>