001package react4j.internal; 002 003import arez.Arez; 004import arez.Observer; 005import arez.spy.ObservableValueInfo; 006import arez.spy.ObserverInfo; 007import grim.annotations.OmitType; 008import java.util.Objects; 009import java.util.stream.Stream; 010import javax.annotation.Nonnull; 011import javax.annotation.Nullable; 012import jsinterop.base.Any; 013import jsinterop.base.Js; 014import jsinterop.base.JsPropertyMap; 015 016/** 017 * Utilities for introspecting the React4j views and runtime. 018 */ 019@OmitType( unless = "react4j.store_debug_data_as_state" ) 020public final class IntrospectUtil 021{ 022 private IntrospectUtil() 023 { 024 } 025 026 /** 027 * Return the value for specified observable. 028 * Exceptions are caught and types are converted to strings using {@link java.lang.Object#toString()} 029 * 030 * @param observableInfo the observable. 031 * @return the value as a string. 032 */ 033 @SuppressWarnings( { "UnnecessaryUnboxing", "rawtypes" } ) 034 @Nullable 035 public static Object getValue( @Nonnull final ObservableValueInfo observableInfo ) 036 { 037 try 038 { 039 if ( Arez.arePropertyIntrospectorsEnabled() && observableInfo.hasAccessor() ) 040 { 041 // Consider unwrapping collections and potentially serializing Arez entities so they are presented correctly in DevTools 042 final Object value = observableInfo.getValue(); 043 if ( null == value ) 044 { 045 return null; 046 } 047 else if ( value instanceof Enum ) 048 { 049 return ( (Enum) value ).name(); 050 } 051 else if ( value instanceof Integer ) 052 { 053 return Js.asAny( ( (Integer) value ).intValue() ); 054 } 055 else if ( value instanceof Boolean ) 056 { 057 return Js.asAny( ( (Boolean) value ).booleanValue() ); 058 } 059 else if ( value instanceof Long ) 060 { 061 return Js.asAny( ( (Long) value ).doubleValue() ); 062 } 063 else if ( value instanceof Float ) 064 { 065 return Js.asAny( ( (Float) value ).doubleValue() ); 066 } 067 else if ( value instanceof Short ) 068 { 069 return Js.asAny( ( (Short) value ).intValue() ); 070 } 071 else if ( value instanceof Byte ) 072 { 073 return Js.asAny( ( (Byte) value ).intValue() ); 074 } 075 else if ( value instanceof Character ) 076 { 077 return value.toString(); 078 } 079 else if ( value instanceof Stream ) 080 { 081 // Streams are new instances every time so render them as strings to avoid infinite loops. 082 return "<Stream>"; 083 } 084 else 085 { 086 return value; 087 } 088 } 089 else 090 { 091 return "?"; 092 } 093 } 094 catch ( final Throwable throwable ) 095 { 096 return throwable; 097 } 098 } 099 100 /** 101 * For the specified observer, collect all dependencies and record them in data to be emitted as debug data. 102 * 103 * @param observer the observer. 104 * @param data the target in which to place debug data. 105 */ 106 public static void collectDependencyDebugData( @Nonnull final Observer observer, 107 @Nonnull final JsPropertyMap<Object> data ) 108 { 109 final ObserverInfo observerInfo = observer.getContext().getSpy().asObserverInfo( observer ); 110 observerInfo.getDependencies().forEach( d -> data.set( d.getName(), getValue( d ) ) ); 111 } 112 113 /** 114 * Prepare the newState value to be updated given specified current state. 115 * If no changes are required then return false. 116 * 117 * @param newState the new "state" of the view. 118 * @param currentState the current "state" of the view. 119 * @return true if newState needs to be saved to native view, false otherwise. 120 */ 121 public static boolean prepareStateUpdate( @Nonnull final JsPropertyMap<Object> newState, 122 @Nullable final JsPropertyMap<Object> currentState ) 123 { 124 /* 125 * To determine whether we need to do a state update we do compare each key and value and make sure 126 * they match. In some cases keys can be removed (i.e. a dependency is no longer observed) but as state 127 * updates in react are merges, we need to implement this by putting undefined values into the state. 128 */ 129 if ( null != currentState ) 130 { 131 boolean[] needsSave = new boolean[ 1 ]; 132 currentState.forEach( key -> { 133 if ( !newState.has( key ) ) 134 { 135 newState.set( key, Js.undefined() ); 136 needsSave[ 0 ] = true; 137 } 138 else 139 { 140 final Any newValue = currentState.getAsAny( key ); 141 final Any existingValue = newState.getAsAny( key ); 142 if ( !Objects.equals( newValue, existingValue ) ) 143 { 144 needsSave[ 0 ] = true; 145 } 146 } 147 } ); 148 return needsSave[ 0 ]; 149 } 150 else 151 { 152 return true; 153 } 154 } 155}