Merge branch 'skin-refactor' into javafx

This commit is contained in:
huanghongxun
2019-02-06 22:53:38 +08:00
40 changed files with 1033 additions and 608 deletions

View File

@@ -69,7 +69,8 @@ public abstract class Account implements Observable {
public abstract Map<Object, Object> toStorage();
public abstract void clearCache();
public void clearCache() {
}
private ObservableHelper helper = new ObservableHelper(this);

View File

@@ -17,27 +17,10 @@
*/
package org.jackhuang.hmcl.auth;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import java.util.List;
import java.util.UUID;
/**
* Select character by name.
* Thrown when a previously existing character cannot be found.
*/
public class SpecificCharacterSelector implements CharacterSelector {
private UUID uuid;
/**
* Constructor.
* @param uuid character's uuid.
*/
public SpecificCharacterSelector(UUID uuid) {
this.uuid = uuid;
}
@Override
public GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException {
return names.stream().filter(profile -> profile.getId().equals(uuid)).findAny().orElseThrow(() -> new NoSelectedCharacterException(account));
public final class CharacterDeletedException extends AuthenticationException {
public CharacterDeletedException() {
}
}

View File

@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.auth;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import java.util.List;
@@ -33,7 +34,6 @@ public interface CharacterSelector {
* @throws NoSelectedCharacterException if cannot select any character may because user close the selection window or cancel the selection.
* @return your choice of game profile.
*/
GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException;
GameProfile select(YggdrasilService yggdrasilService, List<GameProfile> names) throws NoSelectedCharacterException;
CharacterSelector DEFAULT = (account, names) -> names.stream().findFirst().orElseThrow(() -> new NoSelectedCharacterException(account));
}

View File

@@ -22,13 +22,6 @@ package org.jackhuang.hmcl.auth;
* (A account may hold more than one characters.)
*/
public final class NoCharacterException extends AuthenticationException {
private final Account account;
public NoCharacterException(Account account) {
this.account = account;
}
public Account getAccount() {
return account;
public NoCharacterException() {
}
}

View File

@@ -25,17 +25,6 @@ package org.jackhuang.hmcl.auth;
* @author huangyuhui
*/
public final class NoSelectedCharacterException extends AuthenticationException {
private final Account account;
/**
*
* @param account the error yggdrasil account.
*/
public NoSelectedCharacterException(Account account) {
this.account = account;
}
public Account getAccount() {
return account;
public NoSelectedCharacterException() {
}
}

View File

@@ -22,7 +22,6 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.auth.ServerDisconnectException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.util.ToStringBuilder;
@@ -36,14 +35,19 @@ import java.util.concurrent.ExecutionException;
import static java.nio.charset.StandardCharsets.UTF_8;
public class AuthlibInjectorAccount extends YggdrasilAccount {
private AuthlibInjectorServer server;
private final AuthlibInjectorServer server;
private AuthlibInjectorArtifactProvider downloader;
protected AuthlibInjectorAccount(YggdrasilService service, AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, UUID characterUUID, YggdrasilSession session) {
super(service, username, characterUUID, session);
this.downloader = downloader;
public AuthlibInjectorAccount(AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, String password, CharacterSelector selector) throws AuthenticationException {
super(server.getYggdrasilService(), username, password, selector);
this.server = server;
this.downloader = downloader;
}
public AuthlibInjectorAccount(AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, YggdrasilSession session) {
super(server.getYggdrasilService(), username, session);
this.server = server;
this.downloader = downloader;
}
@Override
@@ -52,8 +56,8 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
}
@Override
protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException {
return inject(() -> super.logInWithPassword(password, selector));
public synchronized AuthInfo logInWithPassword(String password) throws AuthenticationException {
return inject(() -> super.logInWithPassword(password));
}
@Override
@@ -121,6 +125,12 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
return map;
}
@Override
public void clearCache() {
super.clearCache();
server.invalidateMetadataCache();
}
public AuthlibInjectorServer getServer() {
return server;
}

View File

@@ -20,7 +20,8 @@ package org.jackhuang.hmcl.auth.authlibinjector;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
import java.util.Map;
import java.util.Objects;
@@ -48,10 +49,7 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
AuthlibInjectorServer server = (AuthlibInjectorServer) additionalData;
AuthlibInjectorAccount account = new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl())),
server, downloader, username, null, null);
account.logInWithPassword(password, selector);
return account;
return new AuthlibInjectorAccount(server, downloader, username, password, selector);
}
@Override
@@ -67,7 +65,14 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
AuthlibInjectorServer server = serverLookup.apply(apiRoot);
return new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl())),
server, downloader, username, session.getSelectedProfile().getId(), session);
tryCast(storage.get("profileProperties"), Map.class).ifPresent(
it -> {
@SuppressWarnings("unchecked")
Map<String, String> properties = it;
GameProfile selected = session.getSelectedProfile();
server.getYggdrasilService().getProfileRepository().put(selected.getId(), new CompleteGameProfile(selected, properties));
});
return new AuthlibInjectorAccount(server, downloader, username, session);
}
}

View File

@@ -56,4 +56,9 @@ public class AuthlibInjectorProvider implements YggdrasilProvider {
public URL getProfilePropertiesURL(UUID uuid) {
return NetworkUtils.toURL(apiRoot + "sessionserver/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid));
}
@Override
public String toString() {
return apiRoot;
}
}

View File

@@ -35,6 +35,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jetbrains.annotations.Nullable;
@@ -143,16 +144,22 @@ public class AuthlibInjectorServer implements Observable {
private transient Map<String, String> links = emptyMap();
private transient boolean metadataRefreshed;
private transient ObservableHelper helper = new ObservableHelper(this);
private final transient ObservableHelper helper = new ObservableHelper(this);
private final transient YggdrasilService yggdrasilService;
public AuthlibInjectorServer(String url) {
this.url = url;
this.yggdrasilService = new YggdrasilService(new AuthlibInjectorProvider(url));
}
public String getUrl() {
return url;
}
public YggdrasilService getYggdrasilService() {
return yggdrasilService;
}
public Optional<String> getMetadataResponse() {
return Optional.ofNullable(metadataResponse);
}
@@ -222,6 +229,10 @@ public class AuthlibInjectorServer implements Observable {
}
}
public void invalidateMetadataCache() {
metadataRefreshed = false;
}
@Override
public int hashCode() {
return url.hashCode();

View File

@@ -25,10 +25,10 @@ import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
@@ -41,15 +41,13 @@ public class OfflineAccount extends Account {
private final String username;
private final UUID uuid;
OfflineAccount(String username, UUID uuid) {
Objects.requireNonNull(username);
Objects.requireNonNull(uuid);
protected OfflineAccount(String username, UUID uuid) {
this.username = requireNonNull(username);
this.uuid = requireNonNull(uuid);
this.username = username;
this.uuid = uuid;
if (StringUtils.isBlank(username))
if (StringUtils.isBlank(username)) {
throw new IllegalArgumentException("Username cannot be blank");
}
}
@Override
@@ -68,10 +66,7 @@ public class OfflineAccount extends Account {
}
@Override
public AuthInfo logIn() throws AuthenticationException {
if (StringUtils.isBlank(username))
throw new AuthenticationException("Username cannot be empty");
public AuthInfo logIn() {
return new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}");
}
@@ -82,7 +77,7 @@ public class OfflineAccount extends Account {
@Override
public Optional<AuthInfo> playOffline() {
return Optional.empty();
return Optional.of(logIn());
}
@Override
@@ -93,11 +88,6 @@ public class OfflineAccount extends Account {
);
}
@Override
public void clearCache() {
// Nothing to clear.
}
@Override
public String toString() {
return new ToStringBuilder(this)

View File

@@ -0,0 +1,59 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.auth.yggdrasil;
import static java.util.Objects.requireNonNull;
import java.util.Map;
import java.util.UUID;
import org.jackhuang.hmcl.util.Immutable;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.JsonAdapter;
/**
* @author yushijinhun
*/
@Immutable
public class CompleteGameProfile extends GameProfile {
@JsonAdapter(PropertyMapSerializer.class)
private final Map<String, String> properties;
public CompleteGameProfile(UUID id, String name, Map<String, String> properties) {
super(id, name);
this.properties = requireNonNull(properties);
}
public CompleteGameProfile(GameProfile profile, Map<String, String> properties) {
this(profile.getId(), profile.getName(), properties);
}
public Map<String, String> getProperties() {
return properties;
}
@Override
public void validate() throws JsonParseException {
super.validate();
if (properties == null)
throw new JsonParseException("Game profile properties cannot be null");
}
}

View File

@@ -17,36 +17,31 @@
*/
package org.jackhuang.hmcl.auth.yggdrasil;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.Validation;
import static java.util.Objects.requireNonNull;
import java.util.UUID;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.gson.Validation;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.JsonAdapter;
/**
*
* @author huangyuhui
*/
@Immutable
public final class GameProfile implements Validation {
public class GameProfile implements Validation {
@JsonAdapter(UUIDTypeAdapter.class)
private final UUID id;
private final String name;
private final PropertyMap properties;
public GameProfile() {
this(null, null);
}
private final String name;
public GameProfile(UUID id, String name) {
this(id, name, new PropertyMap());
}
public GameProfile(UUID id, String name, PropertyMap properties) {
this.id = id;
this.name = name;
this.properties = properties;
this.id = requireNonNull(id);
this.name = requireNonNull(name);
}
public UUID getId() {
@@ -57,18 +52,11 @@ public final class GameProfile implements Validation {
return name;
}
/**
* @return nullable
*/
public PropertyMap getProperties() {
return properties;
}
@Override
public void validate() throws JsonParseException {
if (id == null)
throw new JsonParseException("Game profile id cannot be null or malformed");
if (StringUtils.isBlank(name))
throw new JsonParseException("Game profile name cannot be null or blank");
throw new JsonParseException("Game profile id cannot be null");
if (name == null)
throw new JsonParseException("Game profile name cannot be null");
}
}

View File

@@ -24,7 +24,6 @@ import java.net.URL;
import java.util.UUID;
public class MojangYggdrasilProvider implements YggdrasilProvider {
public static final MojangYggdrasilProvider INSTANCE = new MojangYggdrasilProvider();
@Override
public URL getAuthenticationURL() {
@@ -50,4 +49,9 @@ public class MojangYggdrasilProvider implements YggdrasilProvider {
public URL getProfilePropertiesURL(UUID uuid) {
return NetworkUtils.toURL("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid));
}
@Override
public String toString() {
return "mojang";
}
}

View File

@@ -1,85 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.auth.yggdrasil;
import com.google.gson.*;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
public final class PropertyMap extends HashMap<String, String> {
public static PropertyMap fromMap(Map<?, ?> map) {
PropertyMap propertyMap = new PropertyMap();
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (entry.getKey() instanceof String && entry.getValue() instanceof String)
propertyMap.put((String) entry.getKey(), (String) entry.getValue());
}
return propertyMap;
}
public static class Serializer implements JsonSerializer<PropertyMap>, JsonDeserializer<PropertyMap> {
public static final Serializer INSTANCE = new Serializer();
private Serializer() {
}
@Override
public PropertyMap deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
PropertyMap result = new PropertyMap();
for (JsonElement element : json.getAsJsonArray())
if (element instanceof JsonObject) {
JsonObject object = (JsonObject) element;
result.put(object.get("name").getAsString(), object.get("value").getAsString());
}
return result;
}
@Override
public JsonElement serialize(PropertyMap src, Type typeOfSrc, JsonSerializationContext context) {
JsonArray result = new JsonArray();
for (Map.Entry<String, String> entry : src.entrySet()) {
JsonObject object = new JsonObject();
object.addProperty("name", entry.getKey());
object.addProperty("value", entry.getValue());
result.add(object);
}
return result;
}
}
public static class LegacySerializer
implements JsonSerializer<PropertyMap> {
public static final LegacySerializer INSTANCE = new LegacySerializer();
@Override
public JsonElement serialize(PropertyMap src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject result = new JsonObject();
for (PropertyMap.Entry<String, String> entry : src.entrySet()) {
JsonArray values = new JsonArray();
values.add(new JsonPrimitive(entry.getValue()));
result.add(entry.getKey(), values);
}
return result;
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.auth.yggdrasil;
import static java.util.Collections.unmodifiableMap;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
import java.util.Map;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
public class PropertyMapSerializer implements JsonSerializer<Map<String, String>>, JsonDeserializer<Map<String, String>> {
@Override
public Map<String, String> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
Map<String, String> result = new LinkedHashMap<>();
for (JsonElement element : json.getAsJsonArray())
if (element instanceof JsonObject) {
JsonObject object = (JsonObject) element;
result.put(object.get("name").getAsString(), object.get("value").getAsString());
}
return unmodifiableMap(result);
}
@Override
public JsonElement serialize(Map<String, String> src, Type typeOfSrc, JsonSerializationContext context) {
JsonArray result = new JsonArray();
src.forEach((k, v) -> {
JsonObject object = new JsonObject();
object.addProperty("name", k);
object.addProperty("value", v);
result.add(object);
});
return result;
}
}

View File

@@ -40,10 +40,7 @@ public final class Texture {
return url;
}
public String getMetadata(String key) {
if (metadata == null)
return null;
else
return metadata.get(key);
public Map<String, String> getMetadata() {
return metadata;
}
}

View File

@@ -0,0 +1,43 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.auth.yggdrasil;
import java.util.Map;
import java.util.UUID;
public enum TextureModel {
STEVE("default"), ALEX("slim");
public final String modelName;
private TextureModel(String modelName) {
this.modelName = modelName;
}
public static TextureModel detectModelName(Map<String, String> metadata) {
if (metadata != null && "slim".equals(metadata.get("model"))) {
return ALEX;
} else {
return STEVE;
}
}
public static TextureModel detectUUID(UUID uuid) {
return (uuid.hashCode() & 1) == 1 ? ALEX : STEVE;
}
}

View File

@@ -18,23 +18,31 @@
package org.jackhuang.hmcl.auth.yggdrasil;
import com.google.gson.JsonParseException;
import java.util.Map;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jetbrains.annotations.Nullable;
/**
*
* @author huang
*/
@Immutable
public final class User implements Validation {
private final String id;
private final PropertyMap properties;
@Nullable
private final Map<String, String> properties;
public User(String id) {
this(id, null);
}
public User(String id, PropertyMap properties) {
public User(String id, Map<String, String> properties) {
this.id = id;
this.properties = properties;
}
@@ -43,7 +51,7 @@ public final class User implements Validation {
return id;
}
public PropertyMap getProperties() {
public Map<String, String> getProperties() {
return properties;
}
@@ -52,5 +60,4 @@ public final class User implements Validation {
if (StringUtils.isBlank(id))
throw new JsonParseException("User id cannot be empty.");
}
}

View File

@@ -17,32 +17,62 @@
*/
package org.jackhuang.hmcl.auth.yggdrasil;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.util.StringUtils;
import static java.util.Objects.requireNonNull;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterDeletedException;
import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.auth.CredentialExpiredException;
import org.jackhuang.hmcl.auth.NoCharacterException;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import java.util.*;
/**
*
* @author huangyuhui
*/
public class YggdrasilAccount extends Account {
private final String username;
private final YggdrasilService service;
private boolean isOnline = false;
private final UUID characterUUID;
private final String username;
private boolean authenticated = false;
private YggdrasilSession session;
private UUID characterUUID;
protected YggdrasilAccount(YggdrasilService service, String username, UUID characterUUID, YggdrasilSession session) {
this.service = service;
this.username = username;
this.session = session;
this.characterUUID = characterUUID;
protected YggdrasilAccount(YggdrasilService service, String username, YggdrasilSession session) {
this.service = requireNonNull(service);
this.username = requireNonNull(username);
this.characterUUID = requireNonNull(session.getSelectedProfile().getId());
this.session = requireNonNull(session);
}
if (session == null || session.getSelectedProfile() == null || StringUtils.isBlank(session.getAccessToken()))
this.session = null;
protected YggdrasilAccount(YggdrasilService service, String username, String password, CharacterSelector selector) throws AuthenticationException {
this.service = requireNonNull(service);
this.username = requireNonNull(username);
YggdrasilSession acquiredSession = service.authenticate(username, password, randomClientToken());
if (acquiredSession.getSelectedProfile() == null) {
if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().isEmpty()) {
throw new NoCharacterException();
}
GameProfile characterToSelect = selector.select(service, acquiredSession.getAvailableProfiles());
session = service.refresh(
acquiredSession.getAccessToken(),
acquiredSession.getClientToken(),
characterToSelect);
// response validity has been checked in refresh()
} else {
session = acquiredSession;
}
characterUUID = session.getSelectedProfile().getId();
authenticated = true;
}
@Override
@@ -55,22 +85,20 @@ public class YggdrasilAccount extends Account {
return session.getSelectedProfile().getName();
}
public boolean isLoggedIn() {
return session != null && StringUtils.isNotBlank(session.getAccessToken());
}
public boolean canPlayOnline() {
return isLoggedIn() && session.getSelectedProfile() != null && isOnline;
@Override
public UUID getUUID() {
return session.getSelectedProfile().getId();
}
@Override
public synchronized AuthInfo logIn() throws AuthenticationException {
if (!canPlayOnline()) {
if (!authenticated) {
if (service.validate(session.getAccessToken(), session.getClientToken())) {
isOnline = true;
authenticated = true;
} else {
YggdrasilSession acquiredSession;
try {
updateSession(service.refresh(session.getAccessToken(), session.getClientToken(), null), new SpecificCharacterSelector(characterUUID));
acquiredSession = service.refresh(session.getAccessToken(), session.getClientToken(), null);
} catch (RemoteAuthenticationException e) {
if ("ForbiddenOperationException".equals(e.getRemoteName())) {
throw new CredentialExpiredException(e);
@@ -78,95 +106,85 @@ public class YggdrasilAccount extends Account {
throw e;
}
}
if (acquiredSession.getSelectedProfile() == null ||
!acquiredSession.getSelectedProfile().getId().equals(characterUUID)) {
throw new ServerResponseMalformedException("Selected profile changed");
}
session = acquiredSession;
authenticated = true;
invalidate();
}
}
return session.toAuthInfo();
}
@Override
public synchronized AuthInfo logInWithPassword(String password) throws AuthenticationException {
return logInWithPassword(password, new SpecificCharacterSelector(characterUUID));
}
YggdrasilSession acquiredSession = service.authenticate(username, password, randomClientToken());
protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException {
updateSession(service.authenticate(username, password, UUIDTypeAdapter.fromUUID(UUID.randomUUID())), selector);
return session.toAuthInfo();
}
/**
* Updates the current session. This method shall be invoked after authenticate/refresh operation.
* {@link #session} field shall be set only using this method. This method ensures {@link #session}
* has a profile selected.
*
* @param acquiredSession the session acquired by making an authenticate/refresh request
*/
private void updateSession(YggdrasilSession acquiredSession, CharacterSelector selector) throws AuthenticationException {
if (acquiredSession.getSelectedProfile() == null) {
if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().length == 0)
throw new NoCharacterException(this);
if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().isEmpty()) {
throw new CharacterDeletedException();
}
this.session = service.refresh(
GameProfile characterToSelect = acquiredSession.getAvailableProfiles().stream()
.filter(charatcer -> charatcer.getId().equals(characterUUID))
.findFirst()
.orElseThrow(CharacterDeletedException::new);
session = service.refresh(
acquiredSession.getAccessToken(),
acquiredSession.getClientToken(),
selector.select(this, Arrays.asList(acquiredSession.getAvailableProfiles())));
characterToSelect);
} else {
this.session = acquiredSession;
if (!acquiredSession.getSelectedProfile().getId().equals(characterUUID)) {
throw new CharacterDeletedException();
}
session = acquiredSession;
}
this.characterUUID = this.session.getSelectedProfile().getId();
authenticated = true;
invalidate();
return session.toAuthInfo();
}
@Override
public Optional<AuthInfo> playOffline() {
if (isLoggedIn() && session.getSelectedProfile() != null && !canPlayOnline())
return Optional.of(session.toAuthInfo());
return Optional.empty();
return Optional.of(session.toAuthInfo());
}
@Override
public Map<Object, Object> toStorage() {
if (session == null)
throw new IllegalStateException("No session is specified");
HashMap<Object, Object> storage = new HashMap<>();
storage.put("username", getUsername());
Map<Object, Object> storage = new HashMap<>();
storage.put("username", username);
storage.putAll(session.toStorage());
service.getProfileRepository().getImmediately(characterUUID).ifPresent(profile -> {
storage.put("profileProperties", profile.getProperties());
});
return storage;
}
@Override
public UUID getUUID() {
if (session == null || session.getSelectedProfile() == null)
return null;
else
return session.getSelectedProfile().getId();
}
public Optional<Texture> getSkin() throws AuthenticationException {
return getSkin(session.getSelectedProfile());
}
public Optional<Texture> getSkin(GameProfile profile) throws AuthenticationException {
if (!service.getTextures(profile).isPresent()) {
profile = service.getCompleteGameProfile(profile.getId()).orElse(profile);
}
return service.getTextures(profile).map(map -> map.get(TextureType.SKIN));
public YggdrasilService getYggdrasilService() {
return service;
}
@Override
public void clearCache() {
Optional.ofNullable(session)
.map(YggdrasilSession::getSelectedProfile)
.map(GameProfile::getProperties)
.ifPresent(it -> it.remove("textures"));
authenticated = false;
service.getProfileRepository().invalidate(characterUUID);
}
private static String randomClientToken() {
return UUIDTypeAdapter.fromUUID(UUID.randomUUID());
}
@Override
public String toString() {
return "YggdrasilAccount[username=" + getUsername() + "]";
return "YggdrasilAccount[uuid=" + characterUUID + ", username=" + username + "]";
}
@Override

View File

@@ -20,11 +20,9 @@ package org.jackhuang.hmcl.auth.yggdrasil;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import static org.jackhuang.hmcl.util.Lang.tryCast;
@@ -34,10 +32,12 @@ import static org.jackhuang.hmcl.util.Lang.tryCast;
*/
public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
private final YggdrasilProvider provider;
public static final YggdrasilAccountFactory MOJANG = new YggdrasilAccountFactory(YggdrasilService.MOJANG);
public YggdrasilAccountFactory(YggdrasilProvider provider) {
this.provider = provider;
private YggdrasilService service;
public YggdrasilAccountFactory(YggdrasilService service) {
this.service = service;
}
@Override
@@ -46,9 +46,7 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
Objects.requireNonNull(username);
Objects.requireNonNull(password);
YggdrasilAccount account = new YggdrasilAccount(new YggdrasilService(provider), username, null, null);
account.logInWithPassword(password, selector);
return account;
return new YggdrasilAccount(service, username, password, selector);
}
@Override
@@ -60,10 +58,14 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
String username = tryCast(storage.get("username"), String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have username"));
return new YggdrasilAccount(new YggdrasilService(provider), username, session.getSelectedProfile().getId(), session);
}
tryCast(storage.get("profileProperties"), Map.class).ifPresent(
it -> {
@SuppressWarnings("unchecked")
Map<String, String> properties = it;
GameProfile selected = session.getSelectedProfile();
service.getProfileRepository().put(selected.getId(), new CompleteGameProfile(selected, properties));
});
public static String randomToken() {
return UUIDTypeAdapter.fromUUID(UUID.randomUUID());
return new YggdrasilAccount(service, username, session);
}
}

View File

@@ -27,21 +27,44 @@ import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.gson.ValidationTypeAdapterFactory;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.unmodifiableList;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
public class YggdrasilService {
private static final ThreadPoolExecutor POOL = threadPool("ProfileProperties", true, 2, 10, TimeUnit.SECONDS);
public static final YggdrasilService MOJANG = new YggdrasilService(new MojangYggdrasilProvider());
private final YggdrasilProvider provider;
private final ObservableOptionalCache<UUID, CompleteGameProfile, AuthenticationException> profileRepository;
public YggdrasilService(YggdrasilProvider provider) {
this.provider = provider;
this.profileRepository = new ObservableOptionalCache<>(
uuid -> {
LOG.info("Fetching properties of " + uuid + " from " + provider);
return getCompleteGameProfile(uuid);
},
(uuid, e) -> LOG.log(Level.WARNING, "Failed to fetch properties of " + uuid + " from " + provider, e),
POOL);
}
public ObservableOptionalCache<UUID, CompleteGameProfile, AuthenticationException> getProfileRepository() {
return profileRepository;
}
public YggdrasilSession authenticate(String username, String password, String clientToken) throws AuthenticationException {
@@ -62,7 +85,7 @@ public class YggdrasilService {
return handleAuthenticationResponse(request(provider.getAuthenticationURL(), request), clientToken);
}
private Map<String, Object> createRequestWithCredentials(String accessToken, String clientToken) {
private static Map<String, Object> createRequestWithCredentials(String accessToken, String clientToken) {
Map<String, Object> request = new HashMap<>();
request.put("accessToken", accessToken);
request.put("clientToken", clientToken);
@@ -82,7 +105,16 @@ public class YggdrasilService {
pair("name", characterToSelect.getName())));
}
return handleAuthenticationResponse(request(provider.getRefreshmentURL(), request), clientToken);
YggdrasilSession response = handleAuthenticationResponse(request(provider.getRefreshmentURL(), request), clientToken);
if (characterToSelect != null) {
if (response.getSelectedProfile() == null ||
!response.getSelectedProfile().getId().equals(characterToSelect.getId())) {
throw new ServerResponseMalformedException("Failed to select character");
}
}
return response;
}
public boolean validate(String accessToken) throws AuthenticationException {
@@ -121,20 +153,19 @@ public class YggdrasilService {
* @param uuid the uuid that the character corresponding to.
* @return the complete game profile(filled with more properties)
*/
public Optional<GameProfile> getCompleteGameProfile(UUID uuid) throws AuthenticationException {
public Optional<CompleteGameProfile> getCompleteGameProfile(UUID uuid) throws AuthenticationException {
Objects.requireNonNull(uuid);
return Optional.ofNullable(fromJson(request(provider.getProfilePropertiesURL(uuid), null), GameProfile.class));
return Optional.ofNullable(fromJson(request(provider.getProfilePropertiesURL(uuid), null), CompleteGameProfile.class));
}
public Optional<Map<TextureType, Texture>> getTextures(GameProfile profile) throws AuthenticationException {
public static Optional<Map<TextureType, Texture>> getTextures(CompleteGameProfile profile) throws ServerResponseMalformedException {
Objects.requireNonNull(profile);
Optional<String> encodedTextures = Optional.ofNullable(profile.getProperties())
.flatMap(properties -> Optional.ofNullable(properties.get("textures")));
String encodedTextures = profile.getProperties().get("textures");
if (encodedTextures.isPresent()) {
TextureResponse texturePayload = fromJson(new String(Base64.getDecoder().decode(encodedTextures.get()), UTF_8), TextureResponse.class);
if (encodedTextures != null) {
TextureResponse texturePayload = fromJson(new String(Base64.getDecoder().decode(encodedTextures), UTF_8), TextureResponse.class);
return Optional.ofNullable(texturePayload.textures);
} else {
return Optional.empty();
@@ -148,7 +179,12 @@ public class YggdrasilService {
if (!clientToken.equals(response.clientToken))
throw new AuthenticationException("Client token changed from " + clientToken + " to " + response.clientToken);
return new YggdrasilSession(response.clientToken, response.accessToken, response.selectedProfile, response.availableProfiles, response.user);
return new YggdrasilSession(
response.clientToken,
response.accessToken,
response.selectedProfile,
response.availableProfiles == null ? null : unmodifiableList(response.availableProfiles),
response.user);
}
private static void requireEmpty(String response) throws AuthenticationException {
@@ -168,7 +204,7 @@ public class YggdrasilService {
}
}
private String request(URL url, Object payload) throws AuthenticationException {
private static String request(URL url, Object payload) throws AuthenticationException {
try {
if (payload == null)
return NetworkUtils.doGet(url);
@@ -187,26 +223,25 @@ public class YggdrasilService {
}
}
private class TextureResponse {
private static class TextureResponse {
public Map<TextureType, Texture> textures;
}
private class AuthenticationResponse extends ErrorResponse {
private static class AuthenticationResponse extends ErrorResponse {
public String accessToken;
public String clientToken;
public GameProfile selectedProfile;
public GameProfile[] availableProfiles;
public List<GameProfile> availableProfiles;
public User user;
}
private class ErrorResponse {
private static class ErrorResponse {
public String error;
public String errorMessage;
public String cause;
}
private static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(PropertyMap.class, PropertyMap.Serializer.INSTANCE)
.registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE)
.registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE)
.create();

View File

@@ -18,10 +18,11 @@
package org.jackhuang.hmcl.auth.yggdrasil;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
@@ -30,15 +31,16 @@ import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Pair.pair;
@Immutable
public class YggdrasilSession {
private String clientToken;
private String accessToken;
private GameProfile selectedProfile;
private GameProfile[] availableProfiles;
private User user;
private final String clientToken;
private final String accessToken;
private final GameProfile selectedProfile;
private final List<GameProfile> availableProfiles;
private final User user;
public YggdrasilSession(String clientToken, String accessToken, GameProfile selectedProfile, GameProfile[] availableProfiles, User user) {
public YggdrasilSession(String clientToken, String accessToken, GameProfile selectedProfile, List<GameProfile> availableProfiles, User user) {
this.clientToken = clientToken;
this.accessToken = accessToken;
this.selectedProfile = selectedProfile;
@@ -64,7 +66,7 @@ public class YggdrasilSession {
/**
* @return nullable (null if the YggdrasilSession is loaded from storage)
*/
public GameProfile[] getAvailableProfiles() {
public List<GameProfile> getAvailableProfiles() {
return availableProfiles;
}
@@ -78,7 +80,7 @@ public class YggdrasilSession {
String clientToken = tryCast(storage.get("clientToken"), String.class).orElseThrow(() -> new IllegalArgumentException("clientToken is missing"));
String accessToken = tryCast(storage.get("accessToken"), String.class).orElseThrow(() -> new IllegalArgumentException("accessToken is missing"));
String userId = tryCast(storage.get("userid"), String.class).orElseThrow(() -> new IllegalArgumentException("userid is missing"));
PropertyMap userProperties = tryCast(storage.get("userProperties"), Map.class).map(PropertyMap::fromMap).orElse(null);
Map<String, String> userProperties = tryCast(storage.get("userProperties"), Map.class).orElse(null);
return new YggdrasilSession(clientToken, accessToken, new GameProfile(uuid, name), null, new User(userId, userProperties));
}
@@ -107,5 +109,5 @@ public class YggdrasilSession {
Optional.ofNullable(user.getProperties()).map(GSON_PROPERTIES::toJson).orElse("{}"));
}
private static final Gson GSON_PROPERTIES = new GsonBuilder().registerTypeAdapter(PropertyMap.class, PropertyMap.LegacySerializer.INSTANCE).create();
private static final Gson GSON_PROPERTIES = new Gson();
}

View File

@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.util;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -31,8 +32,8 @@ import java.util.function.Supplier;
*/
public class InvocationDispatcher<ARG> implements Consumer<ARG> {
public static <ARG> InvocationDispatcher<ARG> runOn(Consumer<Runnable> executor, Consumer<ARG> action) {
return new InvocationDispatcher<>(arg -> executor.accept(() -> {
public static <ARG> InvocationDispatcher<ARG> runOn(Executor executor, Consumer<ARG> action) {
return new InvocationDispatcher<>(arg -> executor.execute(() -> {
synchronized (action) {
action.accept(arg.get());
}

View File

@@ -18,7 +18,10 @@
package org.jackhuang.hmcl.util;
import java.util.*;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
@@ -172,6 +175,17 @@ public final class Lang {
return thread;
}
public static ThreadPoolExecutor threadPool(String name, boolean daemon, int threads, long timeout, TimeUnit timeunit) {
AtomicInteger counter = new AtomicInteger(1);
ThreadPoolExecutor pool = new ThreadPoolExecutor(0, threads, timeout, timeunit, new LinkedBlockingQueue<>(), r -> {
Thread t = new Thread(r, name + "-" + counter.getAndIncrement());
t.setDaemon(daemon);
return t;
});
pool.allowsCoreThreadTimeOut();
return pool;
}
public static int parseInt(Object string, int defaultValue) {
try {
return Integer.parseInt(string.toString());

View File

@@ -33,9 +33,6 @@ public final class UUIDTypeAdapter extends TypeAdapter<UUID> {
public static final UUIDTypeAdapter INSTANCE = new UUIDTypeAdapter();
private UUIDTypeAdapter() {
}
@Override
public void write(JsonWriter writer, UUID value) throws IOException {
writer.value(value == null ? null : fromUUID(value));

View File

@@ -19,9 +19,14 @@ package org.jackhuang.hmcl.util.javafx;
import static java.util.Objects.requireNonNull;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Function;
import java.util.function.Supplier;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import javafx.application.Platform;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.value.ObservableValue;
@@ -53,6 +58,10 @@ public abstract class MultiStepBinding<T, U> extends ObjectBinding<U> {
return new FlatMappedBinding<>(map(mapper), nullAlternative);
}
public <V> MultiStepBinding<?, V> asyncMap(Function<U, V> mapper, V initial, Executor executor) {
return new AsyncMappedBinding<>(this, mapper, executor, initial);
}
private static class SimpleBinding<T> extends MultiStepBinding<T, T> {
public SimpleBinding(ObservableValue<T> predecessor) {
@@ -68,6 +77,11 @@ public abstract class MultiStepBinding<T, U> extends ObjectBinding<U> {
public <V> MultiStepBinding<?, V> map(Function<T, V> mapper) {
return new MappedBinding<>(predecessor, mapper);
}
@Override
public <V> MultiStepBinding<?, V> asyncMap(Function<T, V> mapper, V initial, Executor executor) {
return new AsyncMappedBinding<>(predecessor, mapper, executor, initial);
}
}
private static class MappedBinding<T, U> extends MultiStepBinding<T, U> {
@@ -119,4 +133,52 @@ public abstract class MultiStepBinding<T, U> extends ObjectBinding<U> {
}
}
}
private static class AsyncMappedBinding<T, U> extends MultiStepBinding<T, U> {
private final InvocationDispatcher<T> dispatcher;
private boolean initialized = false;
private T prev;
private U value;
public AsyncMappedBinding(ObservableValue<T> predecessor, Function<T, U> mapper, Executor executor, U initial) {
super(predecessor);
this.value = initial;
dispatcher = InvocationDispatcher.runOn(executor, arg -> {
synchronized (this) {
if (initialized && Objects.equals(arg, prev)) {
return;
}
}
U newValue = mapper.apply(arg);
synchronized (this) {
prev = arg;
value = newValue;
initialized = true;
}
Platform.runLater(this::invalidate);
});
}
// called on FX thread, this method is serial
@Override
protected U computeValue() {
T currentPrev = predecessor.getValue();
U value;
boolean updateNeeded = false;
synchronized (this) {
value = this.value;
if (!initialized || !Objects.equals(currentPrev, prev)) {
updateNeeded = true;
}
}
if (updateNeeded) {
dispatcher.accept(currentPrev);
}
return value;
}
}
}

View File

@@ -0,0 +1,162 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.javafx;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
import java.util.function.BiConsumer;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
/**
* @author yushijinhun
*/
public class ObservableCache<K, V, E extends Exception> {
private final ExceptionalFunction<K, V, E> source;
private final BiConsumer<K, Throwable> exceptionHandler;
private final V fallbackValue;
private final Executor executor;
private final ObservableHelper observable = new ObservableHelper();
private final Map<K, V> cache = new HashMap<>();
private final Map<K, CompletableFuture<V>> pendings = new HashMap<>();
private final Map<K, Boolean> invalidated = new HashMap<>();
public ObservableCache(ExceptionalFunction<K, V, E> source, BiConsumer<K, Throwable> exceptionHandler, V fallbackValue, Executor executor) {
this.source = source;
this.exceptionHandler = exceptionHandler;
this.fallbackValue = fallbackValue;
this.executor = executor;
}
public Optional<V> getImmediately(K key) {
synchronized (this) {
return Optional.ofNullable(cache.get(key));
}
}
public void put(K key, V value) {
synchronized (this) {
cache.put(key, value);
invalidated.remove(key);
}
Platform.runLater(observable::invalidate);
}
private CompletableFuture<V> query(K key, Executor executor) {
CompletableFuture<V> future;
synchronized (this) {
CompletableFuture<V> prev = pendings.get(key);
if (prev != null) {
return prev;
} else {
future = new CompletableFuture<>();
pendings.put(key, future);
}
}
executor.execute(() -> {
V result;
try {
result = source.apply(key);
} catch (Throwable ex) {
synchronized (this) {
pendings.remove(key);
}
exceptionHandler.accept(key, ex);
future.completeExceptionally(ex);
return;
}
synchronized (this) {
cache.put(key, result);
invalidated.remove(key);
pendings.remove(key, future);
}
future.complete(result);
Platform.runLater(observable::invalidate);
});
return future;
}
public V get(K key) {
V cached;
synchronized (this) {
cached = cache.get(key);
if (cached != null && !invalidated.containsKey(key)) {
return cached;
}
}
try {
return query(key, Runnable::run).join();
} catch (CompletionException | CancellationException ignored) {
}
if (cached == null) {
return fallbackValue;
} else {
return cached;
}
}
public V getDirectly(K key) throws E {
V result = source.apply(key);
put(key, result);
return result;
}
public ObjectBinding<V> binding(K key) {
return Bindings.createObjectBinding(() -> {
V result;
boolean refresh;
synchronized (this) {
result = cache.get(key);
if (result == null) {
result = fallbackValue;
refresh = true;
} else {
refresh = invalidated.containsKey(key);
}
}
if (refresh) {
query(key, executor);
}
return result;
}, observable);
}
public void invalidate(K key) {
synchronized (this) {
if (cache.containsKey(key)) {
invalidated.put(key, Boolean.TRUE);
}
}
Platform.runLater(observable::invalidate);
}
}

View File

@@ -33,6 +33,10 @@ public class ObservableHelper implements Observable, InvalidationListener {
private List<InvalidationListener> listeners = new CopyOnWriteArrayList<>();
private Observable source;
public ObservableHelper() {
this.source = this;
}
public ObservableHelper(Observable source) {
this.source = source;
}

View File

@@ -0,0 +1,62 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.javafx;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.function.BiConsumer;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import javafx.beans.binding.ObjectBinding;
/**
* @author yushijinhun
*/
public class ObservableOptionalCache<K, V, E extends Exception> {
private final ObservableCache<K, Optional<V>, E> backed;
public ObservableOptionalCache(ExceptionalFunction<K, Optional<V>, E> source, BiConsumer<K, Throwable> exceptionHandler, Executor executor) {
backed = new ObservableCache<>(source, exceptionHandler, Optional.empty(), executor);
}
public Optional<V> getImmediately(K key) {
return backed.getImmediately(key).flatMap(it -> it);
}
public void put(K key, V value) {
backed.put(key, Optional.of(value));
}
public Optional<V> get(K key) {
return backed.get(key);
}
public Optional<V> getDirectly(K key) throws E {
return backed.getDirectly(key);
}
public ObjectBinding<Optional<V>> binding(K key) {
return backed.binding(key);
}
public void invalidate(K key) {
backed.invalidate(key);
}
}